Showing preview only (978K chars total). Download the full file or copy to clipboard to get everything.
Repository: toucansites/toucan
Branch: main
Commit: c00c213fba90
Files: 217
Total size: 908.3 KB
Directory structure:
gitextract__gpz1oly/
├── .github/
│ └── workflows/
│ ├── actions.yml
│ ├── linux.yml
│ ├── macos.yml
│ ├── pr-for-formula.yml
│ └── tag_actions.yml
├── .gitignore
├── .swift-format
├── .swiftformat
├── .swiftformatignore
├── .swiftheaderignore
├── Docker/
│ ├── Dockerfile
│ └── Dockerfile.testing
├── LICENSE
├── Makefile
├── Package.resolved
├── Package.swift
├── README.md
├── Sources/
│ ├── ToucanCore/
│ │ ├── Extensions/
│ │ │ ├── Dictionary+Extensions.swift
│ │ │ ├── Logging+Extensions.swift
│ │ │ ├── String+Extensions.swift
│ │ │ └── URL+Extensions.swift
│ │ ├── GeneratorInfo.swift
│ │ ├── Logger.swift
│ │ └── ToucanError.swift
│ ├── ToucanMarkdown/
│ │ ├── Markdown/
│ │ │ ├── HTML.swift
│ │ │ ├── HTMLVisitor.swift
│ │ │ ├── MarkdownBlockDirective.swift
│ │ │ └── MarkdownToHTMLRenderer.swift
│ │ ├── MarkdownRenderer.swift
│ │ ├── Outline/
│ │ │ ├── Outline.swift
│ │ │ └── OutlineParser.swift
│ │ ├── ReadingTime/
│ │ │ └── ReadingTimeCalculator.swift
│ │ └── Transformers/
│ │ ├── ContentTransformer.swift
│ │ ├── TransformerExecutor.swift
│ │ └── TransformerPipeline.swift
│ ├── ToucanSDK/
│ │ ├── Behaviors/
│ │ │ ├── Behavior.swift
│ │ │ ├── CompileSASSBehavior.swift
│ │ │ └── MinifyCSSBehavior.swift
│ │ ├── Content/
│ │ │ ├── Content+Query.swift
│ │ │ ├── Content.swift
│ │ │ ├── ContentResolver.swift
│ │ │ ├── ContentTypeResolver.swift
│ │ │ ├── IteratorInfo.swift
│ │ │ ├── Query+Resolve.swift
│ │ │ └── RelationValue.swift
│ │ ├── DateFormats/
│ │ │ ├── DateContext.swift
│ │ │ └── ToucanDateFormatters.swift
│ │ ├── Models/
│ │ │ ├── ContextBundle.swift
│ │ │ ├── Destination.swift
│ │ │ ├── PipelineResult.swift
│ │ │ └── Slug.swift
│ │ ├── Outputs/
│ │ │ ├── ContextBundleToHTMLRenderer.swift
│ │ │ └── ContextBundleToJSONRenderer.swift
│ │ ├── Renderers/
│ │ │ ├── BuildTargetSourceRenderer.swift
│ │ │ └── MustacheRenderer.swift
│ │ ├── Toucan.swift
│ │ ├── Utilities/
│ │ │ ├── Any+AnyCodable.swift
│ │ │ ├── AnyCodable+Json.swift
│ │ │ ├── Array+AnyCodable.swift
│ │ │ ├── ContextKeys.swift
│ │ │ ├── CopyManager.swift
│ │ │ ├── Dictionary+AnyCodable.swift
│ │ │ ├── Encodable+Json.swift
│ │ │ └── FirstSucceeding.swift
│ │ └── Validators/
│ │ ├── BuildTargetSourceValidator.swift
│ │ └── TemplateValidator.swift
│ ├── ToucanSerialization/
│ │ ├── ToucanDecoder.swift
│ │ ├── ToucanDecoderError.swift
│ │ ├── ToucanEncoder.swift
│ │ ├── ToucanEncoderError.swift
│ │ ├── ToucanJSONDecoder.swift
│ │ ├── ToucanJSONEncoder.swift
│ │ ├── ToucanYAMLDecoder.swift
│ │ └── ToucanYAMLEncoder.swift
│ ├── ToucanSource/
│ │ ├── Errors/
│ │ │ ├── ObjectLoaderError.swift
│ │ │ ├── SourceLoaderError.swift
│ │ │ └── TemplateLoaderError.swift
│ │ ├── Extensions/
│ │ │ ├── Decoder+Validate.swift
│ │ │ ├── Dictionary+AnyCodable.swift
│ │ │ └── FileManagerKit+Extensions.swift
│ │ ├── Loaders/
│ │ │ ├── BuildTargetSourceLoader.swift
│ │ │ ├── ObjectLoader.swift
│ │ │ ├── RawContentLoader.swift
│ │ │ └── TemplateLoader.swift
│ │ ├── MarkdownParser.swift
│ │ ├── Models/
│ │ │ ├── BuildTargetSource.swift
│ │ │ ├── BuiltTargetSourceLocations.swift
│ │ │ ├── Markdown.swift
│ │ │ ├── Origin.swift
│ │ │ ├── Path.swift
│ │ │ ├── RawContent.swift
│ │ │ ├── Template.swift
│ │ │ └── View.swift
│ │ └── Objects/
│ │ ├── AnyCodable.swift
│ │ ├── Blocks/
│ │ │ ├── Block+Attribute.swift
│ │ │ ├── Block+Parameter.swift
│ │ │ └── Block.swift
│ │ ├── Config/
│ │ │ ├── Config+Blocks.swift
│ │ │ ├── Config+Contents.swift
│ │ │ ├── Config+DataTypes+Date.swift
│ │ │ ├── Config+DataTypes.swift
│ │ │ ├── Config+Location.swift
│ │ │ ├── Config+Pipelines.swift
│ │ │ ├── Config+Renderer+ParagraphStyles.swift
│ │ │ ├── Config+RendererConfig.swift
│ │ │ ├── Config+Site.swift
│ │ │ ├── Config+Templates.swift
│ │ │ ├── Config+Types.swift
│ │ │ └── Config.swift
│ │ ├── Date/
│ │ │ ├── DateFormatterConfig.swift
│ │ │ └── DateLocalization.swift
│ │ ├── Pipeline/
│ │ │ ├── Pipeline+Assets.swift
│ │ │ ├── Pipeline+ContentTypes.swift
│ │ │ ├── Pipeline+DataTypes+Date.swift
│ │ │ ├── Pipeline+DataTypes.swift
│ │ │ ├── Pipeline+Engine.swift
│ │ │ ├── Pipeline+Output.swift
│ │ │ ├── Pipeline+Scope+Context.swift
│ │ │ ├── Pipeline+Scope.swift
│ │ │ ├── Pipeline+Transformers+Transformer.swift
│ │ │ ├── Pipeline+Transformers.swift
│ │ │ └── Pipeline.swift
│ │ ├── Property/
│ │ │ ├── Property.swift
│ │ │ ├── PropertyType.swift
│ │ │ └── SystemPropertyKeys.swift
│ │ ├── Query/
│ │ │ ├── Condition.swift
│ │ │ ├── Direction.swift
│ │ │ ├── Operator.swift
│ │ │ ├── Order.swift
│ │ │ └── Query.swift
│ │ ├── Relation/
│ │ │ ├── Relation.swift
│ │ │ └── RelationType.swift
│ │ ├── Settings/
│ │ │ └── Settings.swift
│ │ ├── Target/
│ │ │ ├── Target.swift
│ │ │ └── TargetConfig.swift
│ │ └── Types/
│ │ └── ContentType.swift
│ ├── _GitCommitHash/
│ │ ├── git_commit_hash.c
│ │ └── include/
│ │ └── git_commit_hash.h
│ ├── toucan/
│ │ └── Entrypoint.swift
│ ├── toucan-generate/
│ │ └── Entrypoint.swift
│ ├── toucan-init/
│ │ ├── Download.swift
│ │ └── Entrypoint.swift
│ ├── toucan-serve/
│ │ ├── Entrypoint.swift
│ │ └── NotFoundMiddleware.swift
│ └── toucan-watch/
│ └── Entrypoint.swift
├── Tests/
│ ├── ToucanCoreTests/
│ │ ├── Extensions/
│ │ │ ├── StringExtensionsTestSuite.swift
│ │ │ └── URLExtensionsTestSuite.swift
│ │ └── ToucanCoreTestSuite.swift
│ ├── ToucanMarkdownTests/
│ │ ├── ContentRendererTestSuite.swift
│ │ ├── HTMLVisitorTestSuite.swift
│ │ ├── MarkdownBlockDirective+Mock.swift
│ │ ├── MarkdownBlockDirectiveTestSuite.swift
│ │ └── OutlineTestSuite.swift
│ ├── ToucanSDKTests/
│ │ ├── BuildTargetSource/
│ │ │ ├── BuildTargetSourceRendererTestSuite.swift
│ │ │ └── BuildTargetSourceValidatorTestSuite.swift
│ │ ├── Content/
│ │ │ ├── ContentQueryTestSuite.swift
│ │ │ └── ContentResolverTestSuite.swift
│ │ ├── DateFormatter/
│ │ │ └── ToucanDateFormatterTestSuite.swift
│ │ ├── E2ETestSuite.swift
│ │ ├── Files/
│ │ │ ├── MarkdownFile.swift
│ │ │ ├── MustacheFile.swift
│ │ │ ├── RawContentBundle.swift
│ │ │ └── YAMLFile.swift
│ │ ├── Mocks/
│ │ │ ├── Mocks+Blocks.swift
│ │ │ ├── Mocks+BuildTargetSources.swift
│ │ │ ├── Mocks+ContentTypes.swift
│ │ │ ├── Mocks+E2E.swift
│ │ │ ├── Mocks+Files.swift
│ │ │ ├── Mocks+Pipelines.swift
│ │ │ ├── Mocks+RawContents.swift
│ │ │ ├── Mocks+Templates.swift
│ │ │ ├── Mocks+Views.swift
│ │ │ └── Mocks.swift
│ │ ├── Template/
│ │ │ └── TemplateValidatorTestSuite.swift
│ │ ├── Toucan/
│ │ │ └── ToucanTestSuite.swift
│ │ └── Utilities/
│ │ ├── AnyCodableWrapTests.swift
│ │ ├── CopyManagerTestSuite.swift
│ │ ├── PrettyPrint.swift
│ │ ├── RecursiveMergeTests.swift
│ │ ├── SlugTests.swift
│ │ └── UnboxingTestSuite.swift
│ └── ToucanSourceTests/
│ ├── BuildTargetSourceLoaderTestSuite.swift
│ ├── Extensions/
│ │ └── FileManagerKitExtensionsTestSuite.swift
│ ├── Files/
│ │ └── YAMLFile.swift
│ ├── MarkdownParserTestSuite.swift
│ ├── Models/
│ │ └── BuildTargetSourceLocationsTestSuite.swift
│ ├── Objects/
│ │ ├── AnyCodableTestSuite.swift
│ │ ├── Config/
│ │ │ └── ConfigTestSuite.swift
│ │ ├── DateFormatting/
│ │ │ └── DateFormattingTestSuite.swift
│ │ ├── Pipeline/
│ │ │ ├── PipelineContentTypeTestSuite.swift
│ │ │ ├── PipelineScopeContextTestSuite.swift
│ │ │ ├── PipelineScopeTestSuite.swift
│ │ │ ├── PipelineTestSuite.swift
│ │ │ └── PipelineTransformersTestSuite.swift
│ │ ├── Property/
│ │ │ ├── PropertyTestSuite.swift
│ │ │ └── PropertyTypeTestSuite.swift
│ │ ├── Query/
│ │ │ ├── ConditionTestSuite.swift
│ │ │ ├── DirectionTestSuite.swift
│ │ │ ├── OperatorTestSuite.swift
│ │ │ ├── OrderTestSuite.swift
│ │ │ └── QueryTestSuite.swift
│ │ ├── Relation/
│ │ │ ├── RelationTestSuite.swift
│ │ │ └── RelationTypeTestSuite.swift
│ │ ├── Settings/
│ │ │ └── SettingsTestSuite.swift
│ │ ├── Target/
│ │ │ ├── TargetConfigTestSuite.swift
│ │ │ └── TargetTestSuite.swift
│ │ └── Types/
│ │ └── TypesTestSuite.swift
│ ├── RawContentLoaderTestSuite.swift
│ └── TemplateLoaderTestSuite.swift
└── scripts/
├── install-toucan.sh
├── packaging/
│ ├── deb.sh
│ ├── dmg.sh
│ ├── pkg.sh
│ ├── rpm.sh
│ └── toucan.spec
├── run-chmod.sh
└── uninstall-toucan.sh
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/actions.yml
================================================
name: Actions
on:
pull_request:
branches:
- main
jobs:
bb_checks:
name: BB Checks
uses: BinaryBirds/github-workflows/.github/workflows/extra_soundness.yml@main
with:
local_swift_dependencies_check_enabled : true
swiftlang_checks:
name: Swiftlang Checks
uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main
with:
license_header_check_project_name: "Toucan"
format_check_enabled : true
broken_symlink_check_enabled : true
unacceptable_language_check_enabled : true
api_breakage_check_enabled : false
docs_check_enabled : false
license_header_check_enabled : false
shell_check_enabled : false
yamllint_check_enabled : false
python_lint_check_enabled : false
swiftlang_tests:
name: Swiftlang Tests
uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main
with:
enable_windows_checks : false
linux_build_command: "swift test --parallel --enable-code-coverage"
linux_exclude_swift_versions: "[{\"swift_version\": \"5.8\"}, {\"swift_version\": \"5.9\"}, {\"swift_version\": \"5.10\"}, {\"swift_version\": \"6.0\"}, {\"swift_version\": \"nightly\"}, {\"swift_version\": \"nightly-main\"}, {\"swift_version\": \"nightly-6.0\"}, {\"swift_version\": \"nightly-6.1\"}, {\"swift_version\": \"nightly-6.3\"}]"
================================================
FILE: .github/workflows/linux.yml
================================================
name: Build, Test and Upload Linux Binaries for tag
on:
workflow_call:
inputs:
version:
required: true
type: string
run_rpm:
required: false
type: boolean
default: true
run_deb:
required: false
type: boolean
default: true
static_stdlib:
required: false
type: boolean
default: true
jobs:
precheck:
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: check
run: |
if [[ "${{ inputs.run_rpm }}" == "true" || "${{ inputs.run_deb }}" == "true" ]]; then
echo "✅ At least one packaging format enabled"
echo "should_run=true" >> $GITHUB_OUTPUT
else
echo "🚫 Both run_rpm and run_deb are false — skipping workflow"
echo "should_run=false" >> $GITHUB_OUTPUT
fi
build-binaries:
needs: precheck
if: needs.precheck.outputs.should_run == 'true'
runs-on: ubuntu-latest
container:
image: swift:6.3
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Install required Swift tools
run: |
chmod +x ./scripts/packaging/*.sh
apt-get update
apt-get install -y curl git clang libcurl4-openssl-dev libssl-dev libatomic1 zip
- name: Install RPM tooling
if: inputs.run_rpm
run: apt-get install -y rpm
- name: Install DEB tooling
if: inputs.run_deb
run: apt-get install -y dpkg-dev
- name: Build with static stdlib
if: inputs.static_stdlib
run: |
echo "🔧 Building with static Swift stdlib"
swift build -c release -Xswiftc -static-stdlib
- name: Build without static stdlib
if: ${{ !inputs.static_stdlib }}
run: |
echo "🔧 Building without static Swift stdlib"
swift build -c release
- name: Build RPM
if: inputs.run_rpm
run: ./scripts/packaging/rpm.sh ${{ inputs.version }}
- name: Verify RPM
if: inputs.run_rpm
run: |
RPM="build-rpm/toucan-linux-x86_64-${{ inputs.version }}.rpm"
echo "🧪 Verifying $RPM"
rpm -Kv "$RPM"
rpm -qp "$RPM"
echo "✅ RPM passed verification"
- name: Build DEB
if: inputs.run_deb
run: ./scripts/packaging/deb.sh ${{ inputs.version }}
- name: Verify DEB
if: inputs.run_deb
run: |
DEB="build-deb/toucan-linux-amd64-${{ inputs.version }}.deb"
echo "🧪 Verifying $DEB"
dpkg-deb --info "$DEB"
dpkg-deb --contents "$DEB"
echo "✅ DEB passed verification"
- name: Upload Linux artifacts
if: inputs.run_rpm || inputs.run_deb
uses: actions/upload-artifact@v4
with:
name: linux-artifacts
retention-days: 1 #no need to store it for 90 days
path: |
${{ inputs.run_rpm && format('build-rpm/toucan-linux-x86_64-{0}.rpm', inputs.version) || '' }}
${{ inputs.run_rpm && format('build-rpm/toucan-linux-{0}.zip', inputs.version) || '' }}
${{ inputs.run_rpm && format('build-rpm/toucan-linux-{0}.sha256', inputs.version) || '' }}
${{ inputs.run_deb && format('build-deb/toucan-linux-amd64-{0}.deb', inputs.version) || '' }}
test-and-upload:
runs-on: ubuntu-latest
needs: build-binaries
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: linux-artifacts
path: ./packages
- name: Check unpacked structure
run: find packages
- name: Test RPM in Fedora
if: inputs.run_rpm
run: |
docker run --rm -v "$PWD/packages:/packages" fedora \
bash -c "dnf install -y /packages/build-rpm/toucan-linux-x86_64-${{ inputs.version }}.rpm && toucan --version"
- name: Upload RPM binary to tag
if: inputs.run_rpm
uses: AButler/upload-release-assets@v3.0
with:
files: packages/build-rpm/toucan-linux-x86_64-${{ inputs.version }}.rpm
repo-token: ${{ secrets.GITHUB_TOKEN }}
release-tag: ${{ github.ref_name }}
- name: Upload zipped Linux binaries
if: inputs.run_rpm
uses: AButler/upload-release-assets@v3.0
with:
files: packages/build-rpm/toucan-linux-${{ inputs.version }}.zip
repo-token: ${{ secrets.GITHUB_TOKEN }}
release-tag: ${{ github.ref_name }}
- name: Upload SHA256 for zipped Linux binaries
if: inputs.run_rpm
uses: AButler/upload-release-assets@v3.0
with:
files: packages/build-rpm/toucan-linux-${{ inputs.version }}.sha256
repo-token: ${{ secrets.GITHUB_TOKEN }}
release-tag: ${{ github.ref_name }}
- name: Test DEB in Ubuntu
if: inputs.run_deb
run: |
docker run --rm -v "$PWD/packages:/packages" ubuntu \
bash -c '
apt-get update &&
apt-get install -y curl &&
dpkg -i /packages/build-deb/toucan-linux-amd64-${{ inputs.version }}.deb || apt-get install -f -y &&
toucan --version
'
- name: Upload DEB binary to tag
if: inputs.run_deb
uses: AButler/upload-release-assets@v3.0
with:
files: packages/build-deb/toucan-linux-amd64-${{ inputs.version }}.deb
repo-token: ${{ secrets.GITHUB_TOKEN }}
release-tag: ${{ github.ref_name }}
================================================
FILE: .github/workflows/macos.yml
================================================
name: Build and Publish macOS Binaries
on:
workflow_call:
inputs:
version:
required: true
type: string
run_pkg:
required: false
type: boolean
default: true
run_dmg:
required: false
type: boolean
default: true
jobs:
precheck:
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: check
run: |
if [[ "${{ inputs.run_pkg }}" == "true" || "${{ inputs.run_dmg }}" == "true" ]]; then
echo "✅ At least one packaging format enabled"
echo "should_run=true" >> $GITHUB_OUTPUT
else
echo "🚫 Both run_pkg and run_dmg are false — skipping workflow"
echo "should_run=false" >> $GITHUB_OUTPUT
fi
build-binaries:
needs: precheck
if: needs.precheck.outputs.should_run == 'true'
runs-on: macos-14
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Import certificates
uses: apple-actions/import-codesign-certs@v3
with:
p12-file-base64: ${{ secrets.MAC_CERTIFICATES }}
p12-password: ${{ secrets.MAC_CERTIFICATES_PASSWORD }}
- name: Verify certificates
run: |
set -e
# Certs to check
certs=("Developer ID Application" "Developer ID Installer")
# Expiration threshold in days (for warnings)
warning_days=30
warning_secs=$((warning_days * 86400))
for cert_name in "${certs[@]}"; do
# Try to get the certificate
if ! cert_pem=$(security find-certificate -c "$cert_name" -p); then
echo "❌ Certificate '$cert_name' not found in keychain"
exit 1
fi
not_after=$(echo "$cert_pem" | openssl x509 -noout -enddate | cut -d= -f2)
expiry_ts=$(date -j -f "%b %e %T %Y %Z" "$not_after" +%s 2>/dev/null || date -d "$not_after" +%s)
now_ts=$(date +%s)
if [ "$expiry_ts" -le "$now_ts" ]; then
echo "❌ Certificate '$cert_name' is expired (expired on $not_after)"
exit 1
fi
if [ $((expiry_ts - now_ts)) -le "$warning_secs" ]; then
echo "⚠️ Certificate '$cert_name' expires soon on $not_after"
fi
done
- name: Install Swift 6.3.1
run: |
curl -L https://download.swift.org/swift-6.3.1-release/xcode/swift-6.3.1-RELEASE/swift-6.3.1-RELEASE-osx.pkg -o /tmp/swift.pkg
sudo installer -pkg /tmp/swift.pkg -target /
TOOLCHAIN_PATH="/Library/Developer/Toolchains/swift-6.3.1-RELEASE.xctoolchain/usr/bin"
echo "$TOOLCHAIN_PATH" >> $GITHUB_PATH
export PATH="$TOOLCHAIN_PATH:$PATH"
swift --version
- name: Check for uncommitted changes
run: |
git_status=$(git status --porcelain)
if [[ -n "$git_status" ]]; then
echo "❌ Uncommitted changes detected:"
echo "$git_status"
exit 1
else
echo "✅ Working directory is clean. No uncommitted changes."
fi
- name: Build Swift binaries for arm64 and x86_64
if: inputs.run_pkg
run: |
chmod +x scripts/packaging/pkg.sh
swift build -c release --arch arm64
swift build -c release --arch x86_64
- name: Package .pkg and .zip
if: inputs.run_pkg
run: scripts/packaging/pkg.sh ${{ inputs.version }}
env:
MAC_APP_IDENTITY: ${{ secrets.MAC_APP_IDENTITY }}
MAC_INSTALLER_IDENTITY: ${{ secrets.MAC_INSTALLER_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APP_SPECIFIC_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }}
- name: Verify .pkg file
if: inputs.run_pkg
run: |
PKG="release/toucan-macos-${{ inputs.version }}.pkg"
echo "🧪 Verifying $PKG"
pkgutil --payload-files "$PKG"
echo "✅ PKG passed verification"
- name: Test installing .pkg file
if: inputs.run_pkg
run: |
PKG="release/toucan-macos-${{ inputs.version }}.pkg"
echo "📦 Installing $PKG to /"
sudo installer -pkg "$PKG" -target /
echo "🔍 Checking for installed binaries"
ls -lh /usr/local/bin/toucan*
echo "📈 Version output:"
/usr/local/bin/toucan --version || echo "⚠️ toucan binary failed to run"
- name: Upload .pkg file to tag
if: inputs.run_pkg
uses: AButler/upload-release-assets@v3.0
with:
files: |
release/toucan-macos-${{ inputs.version }}.pkg
repo-token: ${{ secrets.GITHUB_TOKEN }}
release-tag: ${{ github.ref_name }}
- name: Upload .zip to tag
if: inputs.run_pkg
uses: AButler/upload-release-assets@v3.0
with:
files: release/toucan-macos-${{ inputs.version }}.zip
repo-token: ${{ secrets.GITHUB_TOKEN }}
release-tag: ${{ github.ref_name }}
- name: Upload SHA256 to tag
if: inputs.run_pkg
uses: AButler/upload-release-assets@v3.0
with:
files: release/toucan-macos-${{ inputs.version }}.sha256
repo-token: ${{ secrets.GITHUB_TOKEN }}
release-tag: ${{ github.ref_name }}
- name: Create .dmg file
if: inputs.run_pkg && inputs.run_dmg
run: ./scripts/packaging/dmg.sh ${{ inputs.version }}
env:
MAC_APP_IDENTITY: ${{ secrets.MAC_APP_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APP_SPECIFIC_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }}
- name: Verify .dmg file structure and integrity
if: inputs.run_pkg && inputs.run_dmg
run: |
DMG="release/toucan-macos-${{ inputs.version }}.dmg"
echo "🧪 Verifying structure of $DMG"
hdiutil verify "$DMG"
echo "✅ Verified: $DMG is structurally valid"
- name: Upload .dmg file to tag
if: inputs.run_pkg && inputs.run_dmg
uses: AButler/upload-release-assets@v3.0
with:
files: |
release/toucan-macos-${{ inputs.version }}.dmg
repo-token: ${{ secrets.GITHUB_TOKEN }}
release-tag: ${{ github.ref_name }}
================================================
FILE: .github/workflows/pr-for-formula.yml
================================================
name: Push Homebrew Formula
on:
workflow_call:
inputs:
version:
required: true
type: string
baseurl:
required: true
type: string
homepage:
required: true
type: string
formulafile:
required: true
type: string
formulaclass:
required: true
type: string
repository:
required: true
type: string
secrets:
HOMEBREW_TAP_PAT:
required: true
jobs:
push_formula:
runs-on: ubuntu-latest
steps:
- name: Checkout Tap Repo
uses: actions/checkout@v4
with:
repository: ${{ inputs.repository }}
token: ${{ secrets.HOMEBREW_TAP_PAT }}
ref: main
- name: Set Version Variables
run: |
echo "VERSION=${{ inputs.version }}" >> $GITHUB_ENV
echo "BASE_URL=${{ inputs.baseurl }}" >> $GITHUB_ENV
echo "HOMEPAGE=${{ inputs.homepage }}" >> $GITHUB_ENV
- name: Download SHA256 files from release
run: |
curl -LO "$BASE_URL/toucan-linux-$VERSION.sha256"
curl -LO "$BASE_URL/toucan-macos-$VERSION.sha256"
- name: Read SHA256 values
id: shas
run: |
macos_sha=$(awk '{print $1}' "toucan-macos-$VERSION.sha256")
linux_sha=$(awk '{print $1}' "toucan-linux-$VERSION.sha256")
echo "macos_sha=$macos_sha" >> $GITHUB_OUTPUT
echo "linux_sha=$linux_sha" >> $GITHUB_OUTPUT
- name: Write Formula File
run: |
mkdir -p Formula
cat > Formula/${{ inputs.formulafile }} <<EOF
class ${{ inputs.formulaclass }} < Formula
desc "Toucan is a static site generator written in Swift."
homepage "$HOMEPAGE"
version "$VERSION"
if OS.mac?
url "$BASE_URL/toucan-macos-$VERSION.zip"
sha256 "${{ steps.shas.outputs.macos_sha }}"
elsif OS.linux?
url "$BASE_URL/toucan-linux-$VERSION.zip"
sha256 "${{ steps.shas.outputs.linux_sha }}"
end
def install
bin.install "toucan"
bin.install "toucan-init"
bin.install "toucan-generate"
bin.install "toucan-serve"
bin.install "toucan-watch"
end
test do
assert_match "Usage", shell_output("\#{bin}/toucan --help")
end
end
EOF
- name: Clean up SHA256 files
run: rm -f *.sha256
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.HOMEBREW_TAP_PAT }}
commit-message: "Update formula to version ${{ env.VERSION }}"
branch: feature/update-formula-${{ env.VERSION }}
base: main
title: "Update Homebrew formula to ${{ env.VERSION }}"
body: |
This PR updates the Homebrew formula to version `${{ env.VERSION }}`.
================================================
FILE: .github/workflows/tag_actions.yml
================================================
name: Dispatch macOS and Linux Builds on new tag
on:
push:
tags:
- 'v*'
- '[0-9]*'
permissions:
contents: write
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.extract.outputs.version }}
original_version: ${{ steps.extract.outputs.original_version }}
steps:
- name: Extract version from tag
id: extract
run: |
RAW_VERSION="${GITHUB_REF#refs/tags/}"
CLEAN_VERSION="${RAW_VERSION//-/.}"
echo "Version: $CLEAN_VERSION"
echo "VERSION=$CLEAN_VERSION" >> $GITHUB_ENV
echo "version=$CLEAN_VERSION" >> $GITHUB_OUTPUT
linux:
needs: prepare
uses: ./.github/workflows/linux.yml
with:
version: ${{ needs.prepare.outputs.version }}
run_rpm: true
run_deb: true
static_stdlib: true
secrets: inherit
macos:
needs: prepare
uses: ./.github/workflows/macos.yml
with:
version: ${{ needs.prepare.outputs.version }}
run_pkg: true
run_dmg: false
secrets: inherit
pr_for_formula:
name: Create a PR for Homebrew Formula
needs: [prepare, linux, macos]
uses: ./.github/workflows/pr-for-formula.yml
with:
version: ${{ needs.prepare.outputs.version }}
baseurl: https://github.com/toucansites/toucan/releases/download/${{ github.ref_name }}
homepage: https://github.com/toucansites/toucan
formulafile: toucan.rb
formulaclass: Toucan
repository: toucansites/homebrew-toucan
secrets:
HOMEBREW_TAP_PAT: ${{ secrets.HOMEBREW_TAP_PAT }}
================================================
FILE: .gitignore
================================================
.DS_Store
.swiftpm
.build
.vscode
.obsidian
**/dist
**/docs
Tests/sites/benchmark/
Examples/try-o/
================================================
FILE: .swift-format
================================================
{
"fileScopedDeclarationPrivacy" : {
"accessLevel" : "private"
},
"indentation" : {
"spaces" : 4
},
"multiElementCollectionTrailingCommas": true,
"indentConditionalCompilationBlocks" : false,
"indentSwitchCaseLabels" : false,
"lineBreakAroundMultilineExpressionChainComponents" : true,
"lineBreakBeforeControlFlowKeywords" : true,
"lineBreakBeforeEachArgument" : true,
"lineBreakBeforeEachGenericRequirement" : true,
"lineLength" : 80,
"maximumBlankLines" : 1,
"prioritizeKeepingFunctionOutputTogether" : true,
"respectsExistingLineBreaks" : true,
"rules" : {
"AllPublicDeclarationsHaveDocumentation" : true,
"AlwaysUseLowerCamelCase" : false,
"AmbiguousTrailingClosureOverload" : true,
"BeginDocumentationCommentWithOneLineSummary" : false,
"DoNotUseSemicolons" : true,
"DontRepeatTypeInStaticProperties" : false,
"FileScopedDeclarationPrivacy" : true,
"FullyIndirectEnum" : true,
"GroupNumericLiterals" : true,
"IdentifiersMustBeASCII" : true,
"NeverForceUnwrap" : false,
"NeverUseForceTry" : false,
"NeverUseImplicitlyUnwrappedOptionals" : false,
"NoAccessLevelOnExtensionDeclaration" : false,
"NoAssignmentInExpressions" : true,
"NoBlockComments" : true,
"NoCasesWithOnlyFallthrough" : true,
"NoEmptyTrailingClosureParentheses" : true,
"NoLabelsInCasePatterns" : false,
"NoLeadingUnderscores" : false,
"NoParensAroundConditions" : true,
"NoVoidReturnOnFunctionSignature" : true,
"OneCasePerLine" : true,
"OneVariableDeclarationPerLine" : true,
"OnlyOneTrailingClosureArgument" : true,
"OrderedImports" : false,
"ReturnVoidInsteadOfEmptyTuple" : true,
"UseEarlyExits" : false,
"UseLetInEveryBoundCaseVariable" : false,
"UseShorthandTypeNames" : true,
"UseSingleLinePropertyGetter" : false,
"UseSynthesizedInitializer" : true,
"UseTripleSlashForDocumentationComments" : true,
"UseWhereClausesInForLoops" : false,
"ValidateDocumentationComments" : true
},
"spacesAroundRangeFormationOperators" : false,
"tabWidth" : 4,
"version" : 1
}
================================================
FILE: .swiftformat
================================================
--swiftversion 6
--indent 4
--indentstrings true
--smarttabs true
--xcodeindentation enabled
--maxwidth 80
--trimwhitespace always
--self init-only
--elseposition next-line
--guardelse next-line
--ranges nospace
--wraparguments before-first
--wrapparameters before-first
--wrapcollections before-first
--wrapconditions before-first
--enable trailingclosures
--enable todos
--enable preferKeyPath
--enable organizeDeclarations
--organizationmode type
--visibilityorder private, fileprivate, internal, package, public, open
--categorymark "MARK: - %c"
--markcategories false
--enable wrapAttributes
--funcattributes prev-line
--typeattributes prev-line
--storedvarattrs prev-line
--computedvarattrs prev-line
--complexattrs prev-line
--enable trailingCommas
--commas always
--disable wrapSingleLineComments
--enable yodaConditions
--enable acronyms
--acronyms "ID,URL,UUID,HTTP,YML,YAML,JSON,XML"
--preserveacronyms "rootUrl"
================================================
FILE: .swiftformatignore
================================================
Package.swift
================================================
FILE: .swiftheaderignore
================================================
.*
*.c
*.h
*.txt
*.html
*.yaml
README.md
Package.resolved
Makefile
LICENSE
Package.swift
Docker/**
scripts/**
================================================
FILE: Docker/Dockerfile
================================================
FROM swift:6.3-noble AS build
WORKDIR /build
COPY ./Package.* ./
RUN swift package resolve
COPY Sources ./Sources
COPY Tests ./Tests
COPY Package.swift .
COPY Package.resolved .
RUN swift build -c release --static-swift-stdlib
WORKDIR /staging
RUN cp "$(swift build --package-path /build -c release --show-bin-path)/toucan" ./
RUN cp "$(swift build --package-path /build -c release --show-bin-path)/toucan-generate" ./
RUN cp "$(swift build --package-path /build -c release --show-bin-path)/toucan-init" ./
RUN cp "$(swift build --package-path /build -c release --show-bin-path)/toucan-serve" ./
RUN cp "$(swift build --package-path /build -c release --show-bin-path)/toucan-watch" ./
RUN cp "/usr/libexec/swift/linux/swift-backtrace-static" ./
RUN find -L "$(swift build --package-path /build -c release --show-bin-path)/" -regex '.*\.resources$' -exec cp -Ra {} ./ \;
FROM ubuntu:noble
RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
&& apt-get -q update \
&& apt-get -q dist-upgrade -y \
&& apt-get -q install -y tzdata locales curl unzip \
&& ln -fs /usr/share/zoneinfo/UTC /etc/localtime \
&& dpkg-reconfigure -f noninteractive tzdata \
&& locale-gen en_US.UTF-8 \
&& update-locale LANG=en_US.UTF-8 \
&& rm -r /var/lib/apt/lists/*
ENV LANG=en_US.UTF-8
ENV LC_ALL=en_US.UTF-8
RUN useradd --user-group --create-home --system --skel /dev/null --home-dir /app toucan
WORKDIR /app
COPY --from=build --chown=toucan:toucan /staging /app
# ✅ Ensure all files in /app are executable by all users
RUN chmod -R a+rx /app
ENV SWIFT_BACKTRACE=enable=yes,sanitize=yes,threads=all,images=all,interactive=no,swift-backtrace=./swift-backtrace-static
ENV PATH="/app:$PATH"
USER toucan:toucan
EXPOSE 3000
ENTRYPOINT ["/app/toucan"]
CMD ["--help"]
================================================
FILE: Docker/Dockerfile.testing
================================================
FROM swift:6.1
WORKDIR /app
COPY . ./
RUN swift package resolve
RUN swift package clean
RUN swift package update
CMD ["swift", "test", "--parallel", "--enable-code-coverage"]
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2018-2022 Tibor Bödecs
Copyright (c) 2022-2025 Binary Birds Ltd.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: Makefile
================================================
SHELL=/bin/bash
.PHONY: docker
baseUrl = https://raw.githubusercontent.com/BinaryBirds/github-workflows/refs/heads/main/scripts
check: symlinks language deps lint headers
symlinks:
curl -s $(baseUrl)/check-broken-symlinks.sh | bash
language:
curl -s $(baseUrl)/check-unacceptable-language.sh | bash
deps:
curl -s $(baseUrl)/check-local-swift-dependencies.sh | bash
lint:
curl -s $(baseUrl)/run-swift-format.sh | bash
fmt:
swiftformat .
format:
curl -s $(baseUrl)/run-swift-format.sh | bash -s -- --fix
headers:
curl -s $(baseUrl)/check-swift-headers.sh | bash
fix-headers:
curl -s $(baseUrl)/check-swift-headers.sh | bash -s -- --fix
build:
swift build
release:
swift build -c release
test:
swift test --parallel
test-with-coverage:
swift test --parallel --enable-code-coverage
clean:
rm -rf .build
install:
./scripts/install-toucan.sh
uninstall:
./scripts/uninstall-toucan.sh
docker-image:
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t toucan \
-f ./Docker/Dockerfile \
--load \
.
# docker run --rm -v $(pwd):/app/site toucan generate /app/site/src /app/site/dist
# docker run --rm -v $(pwd):/app/site toucan generate ./site/src ./site/dist --base-url "http://localhost:3000"
# docker run --rm -v $(pwd):/app/site --entrypoint /app/toucan toucan generate ./site/src ./site/dist --base-url "http://localhost:3000"
# docker run --rm -p 3000:3000 -v $(pwd):/app/site toucan serve --hostname "0.0.0.0" --port 3000 ./site/dist
# docker run --rm -v $(pwd):/app/site --entrypoint toucan toucansites/toucan generate /app/site/src /app/site/dist
docker-run:
docker run --rm -v $(pwd):/app -it swift:6.1
docker-tests:
docker build -t toucan-tests . -f ./Docker/Dockerfile.testing && docker run --rm toucan-tests
diff:
diff --color=always -r dist-live dist --exclude=api || true
================================================
FILE: Package.resolved
================================================
{
"originHash" : "b0aa8e8208e365a7988670c760c7c1a87737dac7471c3bcff7b3381dbaabf37b",
"pins" : [
{
"identity" : "async-http-client",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swift-server/async-http-client.git",
"state" : {
"revision" : "5dd84c7bb48b348751d7bbe7ba94a17bafdcef37",
"version" : "1.30.2"
}
},
{
"identity" : "file-manager-kit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/binarybirds/file-manager-kit",
"state" : {
"revision" : "89a7485d5564aafd77d846fe4366f7e67d6b4b9b",
"version" : "0.4.0"
}
},
{
"identity" : "filemonitor",
"kind" : "remoteSourceControl",
"location" : "https://github.com/toucansites/FileMonitor",
"state" : {
"revision" : "082b744b35a2f3d53e19bc1925d358590761551f",
"version" : "0.1.0"
}
},
{
"identity" : "hummingbird",
"kind" : "remoteSourceControl",
"location" : "https://github.com/hummingbird-project/hummingbird",
"state" : {
"revision" : "3ae359b1bb1e72378ed43b59fdcd4d44cac5d7a4",
"version" : "2.16.0"
}
},
{
"identity" : "semver.swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/johnfairh/Semver.swift.git",
"state" : {
"revision" : "0a6e2fe061ecb840c9bf80b6427e70ac039239fa",
"version" : "1.2.4"
}
},
{
"identity" : "sourcemapper",
"kind" : "remoteSourceControl",
"location" : "https://github.com/johnfairh/SourceMapper.git",
"state" : {
"revision" : "2c86d8f44beb2c41effa2f9c5f6cf29b871bf3b9",
"version" : "2.0.0"
}
},
{
"identity" : "swift-algorithms",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-algorithms",
"state" : {
"revision" : "87e50f483c54e6efd60e885f7f5aa946cee68023",
"version" : "1.2.1"
}
},
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser",
"state" : {
"revision" : "c5d11a805e765f52ba34ec7284bd4fcd6ba68615",
"version" : "1.7.0"
}
},
{
"identity" : "swift-asn1",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-asn1.git",
"state" : {
"revision" : "eb50cbd14606a9161cbc5d452f18797c90ef0bab",
"version" : "1.7.0"
}
},
{
"identity" : "swift-async-algorithms",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-async-algorithms",
"state" : {
"revision" : "9d349bcc328ac3c31ce40e746b5882742a0d1272",
"version" : "1.1.3"
}
},
{
"identity" : "swift-atomics",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-atomics.git",
"state" : {
"revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7",
"version" : "1.3.0"
}
},
{
"identity" : "swift-certificates",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-certificates.git",
"state" : {
"revision" : "bde8ca32a096825dfce37467137c903418c1893d",
"version" : "1.19.1"
}
},
{
"identity" : "swift-cmark",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-cmark.git",
"state" : {
"revision" : "5d9bdaa4228b381639fff09403e39a04926e2dbe",
"version" : "0.7.1"
}
},
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git",
"state" : {
"revision" : "6675bc0ff86e61436e615df6fc5174e043e57924",
"version" : "1.4.1"
}
},
{
"identity" : "swift-crypto",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-crypto.git",
"state" : {
"revision" : "1b6b2e274e85105bfa155183145a1dcfd63331f1",
"version" : "4.5.0"
}
},
{
"identity" : "swift-css-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/stackotter/swift-css-parser",
"state" : {
"revision" : "6cf16c6696def00a313daef0e29eb27f5c39ece4",
"version" : "0.1.2"
}
},
{
"identity" : "swift-distributed-tracing",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-distributed-tracing.git",
"state" : {
"revision" : "dc4030184203ffafbb2ec614352487235d747fe0",
"version" : "1.4.1"
}
},
{
"identity" : "swift-http-structured-headers",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-http-structured-headers.git",
"state" : {
"revision" : "933538faa42c432d385f02e07df0ace7c5ecfc47",
"version" : "1.7.0"
}
},
{
"identity" : "swift-http-types",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-http-types.git",
"state" : {
"revision" : "45eb0224913ea070ec4fba17291b9e7ecf4749ca",
"version" : "1.5.1"
}
},
{
"identity" : "swift-log",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-log",
"state" : {
"revision" : "2778fd4e5a12a8aaa30a3ee8285f4ce54c5f3181",
"version" : "1.9.1"
}
},
{
"identity" : "swift-markdown",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-markdown",
"state" : {
"revision" : "7d9a5ce307528578dfa777d505496bd5f544ad94",
"version" : "0.7.3"
}
},
{
"identity" : "swift-metrics",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-metrics.git",
"state" : {
"revision" : "d51c8d13fa366eec807eedb4e37daa60ff5bfdd5",
"version" : "2.10.1"
}
},
{
"identity" : "swift-mustache",
"kind" : "remoteSourceControl",
"location" : "https://github.com/hummingbird-project/swift-mustache",
"state" : {
"revision" : "2e2a84698dd8a5fff2fe28857f0f95bb03d21d64",
"version" : "2.0.2"
}
},
{
"identity" : "swift-nio",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio.git",
"state" : {
"revision" : "bdf004b44f77c56fca752cd1cf243c802f8469c9",
"version" : "2.97.0"
}
},
{
"identity" : "swift-nio-extras",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-extras.git",
"state" : {
"revision" : "5a48717e29f62cb8326d6d42e46b562ca93847a6",
"version" : "1.34.0"
}
},
{
"identity" : "swift-nio-http2",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-http2.git",
"state" : {
"revision" : "81cc18264f92cd307ff98430f89372711d4f6fe9",
"version" : "1.43.0"
}
},
{
"identity" : "swift-nio-ssl",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-ssl.git",
"state" : {
"revision" : "3f337058ccd7243c4cac7911477d8ad4c598d4da",
"version" : "2.37.0"
}
},
{
"identity" : "swift-nio-transport-services",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-transport-services.git",
"state" : {
"revision" : "67787bb645a5e67d2edcdfbe48a216cc549222d5",
"version" : "1.28.0"
}
},
{
"identity" : "swift-numerics",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-numerics.git",
"state" : {
"revision" : "0c0290ff6b24942dadb83a929ffaaa1481df04a2",
"version" : "1.1.1"
}
},
{
"identity" : "swift-protobuf",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git",
"state" : {
"revision" : "81558271e243f8f47dfe8e9fdd55f3c2b5413f68",
"version" : "1.37.0"
}
},
{
"identity" : "swift-sass",
"kind" : "remoteSourceControl",
"location" : "https://github.com/johnfairh/swift-sass",
"state" : {
"revision" : "361b70bbb4f038cc850cb2d5e6ef5aa3495cb6ef",
"version" : "3.3.0"
}
},
{
"identity" : "swift-service-context",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-service-context.git",
"state" : {
"revision" : "d0997351b0c7779017f88e7a93bc30a1878d7f29",
"version" : "1.3.0"
}
},
{
"identity" : "swift-service-lifecycle",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swift-server/swift-service-lifecycle.git",
"state" : {
"revision" : "9829955b385e5bb88128b73f1b8389e9b9c3191a",
"version" : "2.11.0"
}
},
{
"identity" : "swift-system",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-system",
"state" : {
"revision" : "7c6ad0fc39d0763e0b699210e4124afd5041c5df",
"version" : "1.6.4"
}
},
{
"identity" : "swiftcommand",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Zollerboy1/SwiftCommand",
"state" : {
"revision" : "28efb038351a8c45010772b407adb9ea02ba67b5",
"version" : "1.4.2"
}
},
{
"identity" : "swiftsoup",
"kind" : "remoteSourceControl",
"location" : "https://github.com/scinfu/SwiftSoup",
"state" : {
"revision" : "6c7915e16f729857aec3e99068c361e58a00ed68",
"version" : "2.13.4"
}
},
{
"identity" : "version",
"kind" : "remoteSourceControl",
"location" : "https://github.com/mxcl/Version",
"state" : {
"revision" : "3043fcd2a50375db76d89ff206a612471833d1c2",
"version" : "2.2.1"
}
},
{
"identity" : "yams",
"kind" : "remoteSourceControl",
"location" : "https://github.com/jpsim/Yams",
"state" : {
"revision" : "3d6871d5b4a5cd519adf233fbb576e0a2af71c17",
"version" : "5.4.0"
}
}
],
"version" : 3
}
================================================
FILE: Package.swift
================================================
// swift-tools-version: 6.1
import PackageDescription
let swiftSettings: [SwiftSetting] = [
.enableExperimentalFeature("StrictConcurrency=complete"),
]
/// Git commit hash information based on context.
var gitCommitHash: String {
if let git = Context.gitInformation {
let base = git.currentCommit
return git.hasUncommittedChanges ? "\(base)-dev" : base
}
return "untracked"
}
let package = Package(
name: "toucan",
platforms: [
.macOS(.v14),
.iOS(.v17),
.tvOS(.v17),
.watchOS(.v10),
.visionOS(.v1),
],
products: [
.executable(name: "toucan", targets: ["toucan"]),
.executable(name: "toucan-init", targets: ["toucan-init"]),
.executable(name: "toucan-generate", targets: ["toucan-generate"]),
.executable(name: "toucan-watch", targets: ["toucan-watch"]),
.executable(name: "toucan-serve", targets: ["toucan-serve"]),
.library(name: "ToucanCore", targets: ["ToucanCore"]),
.library(name: "ToucanSerialization", targets: ["ToucanSerialization"]),
.library(name: "ToucanMarkdown", targets: ["ToucanMarkdown"]),
.library(name: "ToucanSource", targets: ["ToucanSource"]),
.library(name: "ToucanSDK", targets: ["ToucanSDK"]),
],
dependencies: [
.package(
url: "https://github.com/apple/swift-argument-parser",
"1.5.0"..<"1.7.1" // breaks dep graph for 6.0
),
.package(
url: "https://github.com/apple/swift-markdown",
from: "0.6.0"
),
.package(
url: "https://github.com/apple/swift-log",
"1.6.0"..<"1.10.0"
),
.package(
url: "https://github.com/apple/swift-nio",
"2.0.0"..<"2.97.1" // breaks dep graph for 6.0
),
.package(
url: "https://github.com/binarybirds/file-manager-kit",
exact: "0.4.0"
),
.package(
url: "https://github.com/swift-server/async-http-client",
"1.0.0"..<"1.30.3" // swift configuration breaks on 6.3.1
),
.package(
url: "https://github.com/hummingbird-project/hummingbird",
"2.0.0"..<"2.17.0" // swift configuration breaks on 6.3.1
),
.package(
url: "https://github.com/hummingbird-project/swift-mustache",
from: "2.0.0"
),
.package(
url: "https://github.com/jpsim/Yams",
from: "5.4.0"
),
.package(
url: "https://github.com/scinfu/SwiftSoup",
from: "2.8.0"
),
.package(
url: "https://github.com/toucansites/FileMonitor",
from: "0.1.0"
),
.package(
url: "https://github.com/Zollerboy1/SwiftCommand",
from: "1.4.0"
),
.package(
url: "https://github.com/johnfairh/swift-sass",
from: "3.1.0"
),
.package(
url: "https://github.com/stackotter/swift-css-parser",
from: "0.1.2"
),
.package(
url: "https://github.com/mxcl/Version",
from: "2.2.0"
),
],
targets: [
.target(
name: "_GitCommitHash",
cSettings: [
.define("GIT_COMMIT_HASH", to: #""\#(gitCommitHash)""#)
]
),
// MARK: - executable targets
.executableTarget(
name: "toucan",
dependencies: [
.product(
name: "ArgumentParser",
package: "swift-argument-parser"
),
.product(name: "Logging", package: "swift-log"),
.product(name: "SwiftCommand", package: "SwiftCommand"),
.target(name: "ToucanCore"),
],
swiftSettings: swiftSettings
),
.executableTarget(
name: "toucan-init",
dependencies: [
.product(
name: "ArgumentParser",
package: "swift-argument-parser"
),
.product(name: "Logging", package: "swift-log"),
.product(name: "FileManagerKit", package: "file-manager-kit"),
.product(name: "SwiftCommand", package: "SwiftCommand"),
.target(name: "ToucanCore"),
.target(name: "ToucanSource")
],
swiftSettings: swiftSettings
),
.executableTarget(
name: "toucan-generate",
dependencies: [
.product(
name: "ArgumentParser",
package: "swift-argument-parser"
),
.product(name: "Logging", package: "swift-log"),
.target(name: "ToucanSDK"),
],
swiftSettings: swiftSettings
),
.executableTarget(
name: "toucan-watch",
dependencies: [
.product(
name: "ArgumentParser",
package: "swift-argument-parser"
),
.product(name: "Logging", package: "swift-log"),
.product(name: "FileMonitor", package: "FileMonitor"),
.product(name: "SwiftCommand", package: "SwiftCommand"),
.target(name: "ToucanCore"),
],
swiftSettings: swiftSettings
),
.executableTarget(
name: "toucan-serve",
dependencies: [
.product(
name: "ArgumentParser",
package: "swift-argument-parser"
),
.product(name: "Logging", package: "swift-log"),
.product(name: "Hummingbird", package: "hummingbird"),
.target(name: "ToucanCore"),
],
swiftSettings: swiftSettings
),
// MARK: - regular targets
.target(
name: "ToucanCore",
dependencies: [
.product(name: "Logging", package: "swift-log"),
.product(name: "FileManagerKit", package: "file-manager-kit"),
.product(name: "Version", package: "Version"),
.target(name: "_GitCommitHash"),
],
swiftSettings: swiftSettings
),
.target(
name: "ToucanSerialization",
dependencies: [
.product(name: "Yams", package: "yams"),
.target(name: "ToucanCore"),
],
swiftSettings: swiftSettings
),
.target(
name: "ToucanMarkdown",
dependencies: [
// for outline
.product(name: "SwiftSoup", package: "SwiftSoup"),
// for markdown to html
.product(name: "Markdown", package: "swift-markdown"),
// for transformers
.product(name: "SwiftCommand", package: "SwiftCommand"),
.target(name: "ToucanCore"),
],
swiftSettings: swiftSettings
),
.target(
name: "ToucanSource",
dependencies: [
.target(name: "ToucanCore"),
.target(name: "ToucanSerialization"),
],
swiftSettings: swiftSettings
),
.target(
name: "ToucanSDK",
dependencies: [
.product(name: "Mustache", package: "swift-mustache"),
.product(name: "DartSass", package: "swift-sass"),
.product(name: "SwiftCSSParser", package: "swift-css-parser"),
.target(name: "ToucanCore"),
.target(name: "ToucanSerialization"),
.target(name: "ToucanMarkdown"),
.target(name: "ToucanSource"),
],
swiftSettings: swiftSettings
),
// MARK: - test targets
.testTarget(
name: "ToucanCoreTests",
dependencies: [
.target(name: "ToucanCore"),
]
),
.testTarget(
name: "ToucanMarkdownTests",
dependencies: [
.target(name: "ToucanMarkdown"),
]
),
.testTarget(
name: "ToucanSourceTests",
dependencies: [
.target(name: "ToucanCore"),
.target(name: "ToucanSource"),
.product(
name: "FileManagerKitBuilder",
package: "file-manager-kit"
),
]
),
.testTarget(
name: "ToucanSDKTests",
dependencies: [
.target(name: "ToucanSDK"),
.product(
name: "FileManagerKitBuilder",
package: "file-manager-kit"
),
]
),
]
)
================================================
FILE: README.md
================================================
# Toucan
Toucan is a markdown-based Static Site Generator (SSG) written in Swift.
## Installation
## Compile from source
Make sure you have Swift 6+ installed. See [how to install swift](https://www.swift.org/install/) for instructions.
To build Toucan from source, run the following commands:
```shell
# clone the Toucan repository
git clone https://github.com/toucansites/toucan.git
cd toucan
# install Toucan on your system under /usr/local/bin
make install
# enter your password, if needed
# verify
which toucan
# should return /usr/local/bin/toucan
# uninstall, remove Toucan from your system
make uninstall
# enter your password, if needed
```
## Quickstart
To quickly bootstrap a Toucan-based static site, run the following commands:
```shell
toucan init my-site
cd my-site
toucan generate
toucan serve
# Visit: http://localhost:3000
```
## Documentation
The complete documentation for Toucan is available on [toucansites.com](https://toucansites.com/docs/).
================================================
FILE: Sources/ToucanCore/Extensions/Dictionary+Extensions.swift
================================================
//
// Dictionary+Extensions.swift
// Toucan
//
// Created by Tibor Bödecs on 2025. 02. 03..
//
public extension Dictionary {
/// Transforms the keys of the dictionary using the given closure, preserving the associated values.
///
/// This method applies the provided transformation to each key in the dictionary,
/// resulting in a new dictionary with the transformed keys and original values.
///
/// - Parameter t: A closure that takes a key as input and returns a transformed key.
/// - Returns: A dictionary with transformed keys and the original values.
func mapKeys<T>(
_ t: (Key) throws -> T
) rethrows -> [T: Value] {
try .init(
uniqueKeysWithValues: map { try (t($0.key), $0.value) }
)
}
}
/// This extension allows recursive merging of dictionaries with String keys and Any values.
public extension Dictionary where Key == String {
/// Recursively merges another `[String: Value]` dictionary into the current dictionary and returns a new dictionary.
///
/// - Parameter other: The dictionary to merge into the current dictionary.
/// - Returns: A new dictionary with the merged contents.
func recursivelyMerged(
with other: [String: Value]
) -> [String: Value] {
var result = self
for (key, value) in other {
if let existingValue = result[key] as? [String: Value],
let newValue = value as? [String: Value]
{
result[key] =
existingValue.recursivelyMerged(with: newValue) as? Value
}
else {
result[key] = value
}
}
return result
}
/// Retrieves a nested value from the receiver using a dot-separated key path.
/// Supports traversal through dictionaries with `String` keys and arrays with numeric indices.
///
/// - Parameter keyPath: A dot-separated string representing the path to the nested value.
/// - Returns: The value at the specified key path, or `nil` if the path is invalid.
func value(
forKeyPath keyPath: String
) -> Any? {
let keys = keyPath.split(separator: ".").map(String.init)
var current: Any? = self
for key in keys {
if let dict = current as? [String: Any], let next = dict[key] {
current = next
continue
}
if let array = current as? [Any], let index = Int(key),
array.indices.contains(index)
{
current = array[index]
continue
}
return nil
}
return current
}
}
================================================
FILE: Sources/ToucanCore/Extensions/Logging+Extensions.swift
================================================
//
// Logging+Extensions.swift
// Toucan
//
// Created by Tibor Bödecs on 2025. 05. 18..
//
import class Foundation.ProcessInfo
import Logging
public extension Logger {
/// Returns a logger instance for the specified subsystem.
///
/// Constructs a logger with a label based on the subsystem identifier and sets its log level.
/// The log level is determined by checking environment variables
/// for subsystem-specific or global log level settings. If none are found, the provided default level is used.
///
/// - Parameters:
/// - id: The subsystem identifier (e.g., `"generate"`, `"object-loader"`).
/// - level: The default log level to use if not specified elsewhere. Defaults to `.info`.
/// - Returns: A configured `Logger` instance for the subsystem.
static func subsystem(
_ id: String = "",
_ level: Logger.Level = .info
) -> Logger {
var logger = Logger(label: id.loggerLabel())
logger.logLevel = findEnvLogLevel(id) ?? level
return logger
}
}
private extension Logger {
/// Returns the log level from environment variables for the given subsystem identifier.
///
/// Checks for a subsystem-specific log level key and a global log level key (`TOUCAN_LOG_LEVEL`)
/// in the environment. If a valid log level string is found, it is converted to a `Logger.Level`.
///
/// - Parameter id: The subsystem identifier.
/// - Returns: The log level if found and valid, otherwise `nil`.
static func findEnvLogLevel(_ id: String) -> Logger.Level? {
let env = ProcessInfo.processInfo.environment
let keys = [
id.subsystemLogLevelKey(),
"TOUCAN_LOG_LEVEL",
]
for key in keys {
if let rawLevel = env[key]?.lowercased(),
let level = Logger.Level(rawValue: rawLevel)
{
return level
}
}
return nil
}
}
private extension String {
/// Returns the logger label for a subsystem.
///
/// Constructs a logger label by joining "TOUCAN" and the subsystem identifier with a hyphen.
/// If the identifier is empty, returns "toucan".
///
/// - Examples:
/// - For an empty string: `"toucan"`
/// - For `"generate"`: `"toucan-generate"`
/// - For `"object-loader"`: `"toucan-object-loader"`
func loggerLabel() -> String {
let prefix = "toucan"
let parts = isEmpty ? [prefix] : [prefix, self]
return parts.joined(separator: "-")
}
/// Returns the environment variable key for the log level of a subsystem.
///
/// This method constructs a log level key by converting the logger label to uppercase,
/// replacing hyphens with underscores, and appending "_LOG_LEVEL".
///
/// - Examples:
/// - For an empty string: `"TOUCAN_LOG_LEVEL"`
/// - For `"generate"`: `"TOUCAN_GENERATE_LOG_LEVEL"`
/// - For `"object-loader"`: `"TOUCAN_OBJECT_LOADER_LOG_LEVEL"`
func subsystemLogLevelKey() -> String {
loggerLabel()
.uppercased()
.replacing("-", with: "_")
.appending("_LOG_LEVEL")
}
}
================================================
FILE: Sources/ToucanCore/Extensions/String+Extensions.swift
================================================
//
// String+Extensions.swift
// Toucan
//
// Created by Tibor Bödecs on 2025. 05. 17..
//
import Foundation
public extension String {
/// A convenience property that converts an empty string to `nil`.
///
/// This is useful for cases where an empty string should be treated as the absence of a value,
/// such as when preparing optional fields for encoding, form validation, or API payloads.
///
/// For example:
/// ```swift
/// let name: String = ""
/// let optionalName = name.emptyToNil // Result: nil
/// ```
///
/// - Returns: `nil` if the string is empty; otherwise, returns the original string.
var emptyToNil: String? {
isEmpty ? nil : self
}
/// Removes the leading slash from the string if present.
///
/// This method checks if the string starts with a slash (`/`). If so, it removes it.
///
/// - Returns: A new string without a leading slash, or the original string if no leading slash exists.
func dropLeadingSlash() -> String {
if hasPrefix("/") {
return String(dropFirst())
}
return self
}
/// Removes the trailing slash from the string if present.
///
/// This method checks if the string ends with a slash (`/`). If so, it removes it.
///
/// - Returns: A new string without a trailing slash, or the original string if no trailing slash exists.
func dropTrailingSlash() -> String {
if hasSuffix("/") {
return String(dropLast())
}
return self
}
/// Ensures the string starts with a leading slash.
///
/// This method checks if the string already begins with a slash (`/`). If it does, the original string is returned.
/// Otherwise, it prepends a slash to the beginning of the string.
///
/// - Returns: A new string with a leading slash ensured.
func ensureLeadingSlash() -> String {
if hasPrefix("/") {
return self
}
return "/" + self
}
/// Appends a trailing slash to the string if not already present.
///
/// This method checks if the string ends with a slash (`/`). If not, it appends one.
///
/// - Returns: A new string with a trailing slash ensured.
func ensureTrailingSlash() -> String {
if hasSuffix("/") {
return self
}
return self + "/"
}
/// Replaces substrings in the string using a given dictionary of replacements.
///
/// This method iterates over the key-value pairs in the provided dictionary
/// and replaces all occurrences of each key with its corresponding value.
///
/// - Parameter dictionary: A dictionary where each key is a substring to search for,
/// and the corresponding value is the string to replace it with.
/// - Returns: A new string with all specified substrings replaced.
func replacing(
_ dictionary: [String: String]
) -> String {
var result = self
for (key, value) in dictionary {
result = result.replacing(key, with: value)
}
return result
}
/// Converts the string into a URL-friendly slug.
///
/// This method removes diacritics, trims whitespace, lowercases the string,
/// and keeps only alphanumeric characters, dashes, underscores, and periods.
/// Invalid characters are removed, and remaining components are joined with hyphens.
///
/// - Returns: A slugified version of the original string.
func slugify() -> String {
let allowed = CharacterSet(
charactersIn: "abcdefghijklmnopqrstuvwxyz0123456789-_."
)
return trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
.folding(
options: .diacriticInsensitive,
locale: .init(identifier: "en-US")
)
.components(separatedBy: allowed.inverted)
.filter { $0 != "" }
.joined(separator: "-")
}
/// Resolves a relative asset path by combining it with a base URL, assets path, and slug.
///
/// This method builds a complete asset URL by handling various cases:
/// - If the base URL or assets path is empty, it returns the original string.
/// - If the string starts with `/`, it appends the string directly to the base URL.
/// - If the string starts with a relative prefix (e.g., `./assetsPath/`), it removes the prefix
/// and combines the base URL, assets path, slug, and remaining path parts into a full URL.
///
/// - Parameters:
/// - baseURL: The base URL used to form the full path.
/// - assetsPath: The relative directory for the assets.
/// - slug: A string inserted in the final path for identification or grouping.
/// - Returns: A full string URL combining all parts, or the original string if no resolution is applied.
func resolveAsset(
baseURL: String,
assetsPath: String,
slug: String
) -> String {
if baseURL.isEmpty || assetsPath.isEmpty {
return self
}
let baseURL = baseURL.dropTrailingSlash()
if hasPrefix("/") {
return [baseURL, dropLeadingSlash()].joined(separator: "/")
}
let prefix = "./\(assetsPath)/"
guard hasPrefix(prefix) else {
return self
}
let src = String(dropFirst(prefix.count))
return [baseURL, assetsPath, slug, src]
.filter { !$0.isEmpty }
.joined(separator: "/")
}
/// Checks if a string contains only valid URL characters.
///
/// Allowed: unreserved (`A–Z a–z 0–9 - . _ ~`), reserved
/// (`:/?#[]@!$&'()*+,;=`), and `%` for encoding.
///
/// - Returns: `true` if all characters are valid, otherwise `false`.
func containsOnlyValidURLCharacters() -> Bool {
let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
let numerics = "0123456789"
let special = "-._~{}%"
let reserved = ":/?#[]@!$&'()*+,;="
let allowed = CharacterSet(
charactersIn: alphabet + numerics + special + reserved
)
return unicodeScalars.allSatisfy { allowed.contains($0) }
}
/// Checks if a string contains only valid URL characters.
///
/// Allowed: unreserved (`A–Z a–z 0–9 - . _ ~`), reserved
/// (`:/?#[]@!$&'()*+,;=`), and `%` for encoding.
///
/// - Returns: `true` if all characters are valid, otherwise `false`.
func containsOnlyValidPathCharacters() -> Bool {
let disallowed = CharacterSet(
charactersIn: "%?#&="
)
return unicodeScalars.allSatisfy { !disallowed.contains($0) }
}
}
================================================
FILE: Sources/ToucanCore/Extensions/URL+Extensions.swift
================================================
//
// URL+Extensions.swift
// Toucan
//
// Created by Tibor Bödecs on 2025. 05. 17..
//
import Foundation
public extension URL {
/// Returns a new URL by appending the given path component if it is non-nil and not empty.
///
/// This method is useful when working with optional path components where you want to
/// conditionally append the value only if it's meaningful (i.e., not `nil` or an empty string).
///
/// - Parameter path: An optional string representing the path component to append.
/// - Returns: A new `URL` with the appended path component if valid; otherwise, the original URL.
///
/// ## Example
/// ```swift
/// let baseURL = URL(string: "https://example.com/api")!
/// let endpoint: String? = "users"
/// let fullURL = baseURL.appendingPathIfPresent(endpoint)
/// // fullURL: https://example.com/api/users
/// ```
func appendingPathIfPresent(_ path: String?) -> URL {
guard let path, !path.isEmpty else {
return self
}
return appending(path: path)
}
}
================================================
FILE: Sources/ToucanCore/GeneratorInfo.swift
================================================
//
// GeneratorInfo.swift
// Toucan
//
// Created by Viasz-Kádi Ferenc on 2025. 03. 19..
//
import _GitCommitHash
import Version
public extension GeneratorInfo {
/// Returns the most current version of the generator.
static var current: Self {
.v1_0_0
}
}
/// List available releases here
extension GeneratorInfo {
static let v1_0_0 = GeneratorInfo(version: "1.0.0")
static let v1_0_0_rc_1 = GeneratorInfo(version: "1.0.0-rc.1")
static let v1_0_0_beta_6 = GeneratorInfo(version: "1.0.0-beta.6")
static let v1_0_0_beta_5 = GeneratorInfo(version: "1.0.0-beta.5")
static let v1_0_0_beta_4 = GeneratorInfo(version: "1.0.0-beta.4")
static let v1_0_0_beta_3 = GeneratorInfo(version: "1.0.0-beta.3")
static let v1_0_0_beta_2 = GeneratorInfo(version: "1.0.0-beta.2")
static let v1_0_0_beta_1 = GeneratorInfo(version: "1.0.0-beta.1")
}
/// Metadata describing the content generator, including its name, version, and homepage link.
public struct GeneratorInfo: Codable, Sendable {
/// The name of the generator.
public let name: String
/// The version (e.g., `"1.0.0"`, `"1.0.0-beta.4"`).
public let release: Version
/// The git commit hash based on the SPM context.
public var gitCommitHash: String {
.init(cString: git_commit_hash())
}
/// The complete version information based on the version and the git commit hash
public var version: String {
"\(release.description) (\(gitCommitHash))"
}
/// A URL pointing to the generator’s homepage or documentation.
public let link: String
/// Initializes a generator metadata instance.
///
/// - Parameters:
/// - name: The name of the generator (defaults to `"Toucan"`).
/// - version: The generator version string.
/// - link: A link to the project or documentation (defaults to GitHub).
init(
name: String = "Toucan",
version: String,
link: String = "https://github.com/toucansites/toucan"
) {
self.name = name
self.release = Version(version)!
self.link = link
}
}
================================================
FILE: Sources/ToucanCore/Logger.swift
================================================
//
// Logger.swift
// Toucan
//
// Created by Tibor Bödecs on 2025. 05. 18..
//
import Logging
/// A protocol for types that can provide structured metadata for logging.
///
/// Conforming types expose a dictionary of metadata values used to enrich log messages.
public protocol LoggerMetadataRepresentable {
/// A dictionary of key-value pairs representing structured logging metadata.
///
/// This metadata can be used to provide additional context in log output.
var logMetadata: [String: Logger.MetadataValue] { get }
}
================================================
FILE: Sources/ToucanCore/ToucanError.swift
================================================
//
// ToucanError.swift
// Toucan
//
// Created by Tibor Bödecs on 2025. 05. 20..
//
import Foundation
/// A protocol for custom errors used in the Toucan framework.
///
/// Provides properties for logging, user-facing messages, and a method
/// to format nested error messages in a readable stack-like format.
public protocol ToucanError: Error {
/// A developer-facing error description used for logging purposes.
var logMessage: String { get }
/// A simplified error message suitable for display to end users.
var userFriendlyMessage: String { get }
/// A list of underlying errors, useful for representing error hierarchies.
var underlyingErrors: [Error] { get }
/// Generates a readable stack-like message of the error and any underlying errors.
///
/// - Returns: A formatted string detailing the error structure.
func logMessageStack() -> String
/// Searches for an error of a specific type in the error hierarchy.
///
/// This method traverses the list of underlying errors and attempts to cast
/// each one to the specified error type `T`. If a match is found, it is returned.
/// The search is recursive and will descend into nested `ToucanError`s.
///
/// - Parameter errorType: The type of error to search for.
/// - Returns: An instance of the specified error type if found, otherwise `nil`.
func lookup<T: Error>(
_ errorType: T.Type
) -> T?
/// Searches for a specific associated value in the error hierarchy using a custom matcher.
///
/// This method first attempts to locate an error of type `T`, and if successful,
/// applies the provided matcher closure to extract an associated value.
///
/// - Parameter t: A closure that takes an error of type `T` and returns an associated value of type `V?`.
/// - Returns: The extracted associated value if found, otherwise `nil`.
func lookup<T: Error, V>(
_ t: (T) -> V?
) -> V?
}
public extension ToucanError {
/// Searches for an error of a specific type in the error hierarchy.
///
/// This method traverses the list of underlying errors and attempts to cast
/// each one to the specified error type `T`. If a match is found, it is returned.
/// The search is recursive and will descend into nested `ToucanError`s.
///
/// - Parameter errorType: The type of error to search for.
/// - Returns: An instance of the specified error type if found, otherwise `nil`.
func lookup<T: Error>(
_ errorType: T.Type
) -> T? {
for error in underlyingErrors {
if let match = error as? T {
return match
}
if let match = (error as ToucanError).lookup(errorType) {
return match
}
}
return nil
}
/// Searches for a specific associated value in the error hierarchy using a custom matcher.
///
/// This method first attempts to locate an error of type `T`, and if successful,
/// applies the provided matcher closure to extract an associated value.
///
/// - Parameter t: A closure that takes an error of type `T` and returns an associated value of type `V?`.
/// - Returns: The extracted associated value if found, otherwise `nil`.
func lookup<T: Error, V>(
_ t: (T) -> V?
) -> V? {
lookup(T.self).flatMap(t)
}
}
/// Conforms `NSError` to the `ToucanError` protocol, providing
/// default implementations for logging and user-friendly messages.
extension NSError: ToucanError {
/// A detailed log message composed of the domain, code, and localized description.
public var logMessage: String {
"\(domain):\(code) - \(localizedDescription)"
}
/// A user-facing message derived from the localized description.
public var userFriendlyMessage: String {
"\(localizedDescription)"
}
}
/// Provides default implementations for `ToucanError` protocol
/// including empty `underlyingErrors` and a recursive `logMessageStack`.
public extension ToucanError {
/// A default empty list of underlying errors. Can be overridden by conforming types to provide error hierarchies.
var underlyingErrors: [Error] { [] }
/// Recursively builds a string that describes the error and its underlying errors in a readable format.
///
/// - Returns: A formatted stack-like string representing the error and any nested underlying errors.
func logMessageStack() -> String {
format(error: self)
}
/// Recursively formats an error and its underlying errors into a structured log message.
///
/// - Parameters:
/// - error: The error to format.
/// - prefix: The current indentation prefix.
/// - isLast: Indicates whether the error is the last in its group.
/// - Returns: A formatted string representing the error hierarchy.
private func format(
error: Error,
prefix: String = "",
isLast: Bool = true
) -> String {
let type = type(of: error)
var message: String
var underlyingErrors: [Error]
switch error {
case let e as ToucanError:
message = e.logMessage
underlyingErrors = e.underlyingErrors
case let e as LocalizedError:
message = e.localizedDescription
underlyingErrors = []
default:
message = "\(error)"
underlyingErrors = []
}
let branch = prefix.isEmpty ? "" : (isLast ? "└─ " : "├─ ")
var output = "\(prefix)\(branch)\(type): \"\(message)\"\n"
let childPrefix = prefix + (isLast ? " " : "│ ")
let childCount = underlyingErrors.count
for (idx, error) in underlyingErrors.enumerated() {
let lastChild = (idx == childCount - 1)
output += format(
error: error,
prefix: childPrefix,
isLast: lastChild
)
}
return output
}
}
================================================
FILE: Sources/ToucanMarkdown/Markdown/HTML.swift
================================================
//
// HTML.swift
// Toucan
//
// Created by Tibor Bödecs on 2025. 02. 19..
//
struct HTML {
enum TagType {
case standard
case short
}
struct Attribute {
var key: String
var value: String
}
var name: String
var type: TagType
var attributes: [Attribute]
var contents: String?
init(
name: String,
type: TagType = .standard,
attributes: [Attribute] = [],
contents: String? = nil
) {
self.name = name
self.type = type
self.attributes = attributes
self.contents = contents
}
func render() -> String {
let attributeString =
attributes
.map { #"\#($0.key)="\#($0.value)""# }
.joined(separator: " ")
let tag = [name, attributeString]
.filter { !$0.isEmpty }
.joined(separator: " ")
var result = "<\(tag)>"
result += contents ?? ""
if type == .standard {
result += "</\(name)>"
}
return result
}
}
================================================
FILE: Sources/ToucanMarkdown/Markdown/HTMLVisitor.swift
================================================
//
// HTMLVisitor.swift
// Toucan
//
// Created by Tibor Bödecs on 2025. 02. 19..
//
import Logging
import Markdown
import ToucanCore
/// NOTE: https://www.markdownguide.org/basic-syntax/
private extension String {
func escapeAngleBrackets() -> String {
replacing(
[
#"<"#: #"<"#,
#">"#: #">"#,
// #"&"#: #"&"#,
// #"'"#: #"'"#,
// #"""#: #"""#,
]
)
}
}
private extension Markup {
var isInsideList: Bool {
self is ListItemContainer || parent?.isInsideList == true
}
}
private extension [DirectiveArgument] {
func getFirstValueBy(key name: String) -> String? {
first(where: { $0.name == name })?.value
}
}
struct HTMLVisitor: MarkupVisitor {
typealias Result = String
var customBlockDirectives: [MarkdownBlockDirective]
var paragraphStyles: [String: [String]]
var logger: Logger
var slug: String
var assetsPath: String
var baseURL: String
init(
blockDirectives: [MarkdownBlockDirective] = [],
paragraphStyles: [String: [String]],
slug: String,
assetsPath: String,
baseURL: String,
logger: Logger = .subsystem("html-visitor")
) {
self.customBlockDirectives = blockDirectives
self.paragraphStyles = paragraphStyles
self.slug = slug
self.assetsPath = assetsPath
self.baseURL = baseURL
self.logger = logger
}
// MARK: - visitor functions
private mutating func visit(
_ children: MarkupChildren
) -> Result {
var result = ""
for child in children {
result += visit(child)
}
return result
}
mutating func defaultVisit(
_ markup: any Markup
) -> Result {
visit(markup.children)
}
mutating func visitText(
_ text: Text
) -> Result {
text.plainText
}
mutating func visitHTMLBlock(
_ html: HTMLBlock
) -> Result {
html.rawHTML //.escapeAngleBrackets()
}
mutating func visitInlineHTML(
_ inlineHTML: InlineHTML
) -> Result {
inlineHTML.rawHTML.escapeAngleBrackets()
}
// MARK: - simple HTML elements
mutating func visitSoftBreak(
_: SoftBreak
) -> Result {
HTML(name: "br", type: .short).render()
}
mutating func visitLineBreak(
_: LineBreak
) -> Result {
HTML(name: "br", type: .short).render()
}
mutating func visitThematicBreak(
_: ThematicBreak
) -> Result {
HTML(name: "hr", type: .short).render()
}
mutating func visitListItem(
_ listItem: ListItem
) -> Result {
HTML(name: "li", contents: visit(listItem.children)).render()
}
mutating func visitOrderedList(
_ orderedList: OrderedList
) -> Result {
var attributes: [HTML.Attribute] = []
if orderedList.startIndex > 1 {
attributes.append(
.init(
key: "start",
value: String(
orderedList.startIndex
)
)
)
}
return HTML(
name: "ol",
attributes: attributes,
contents: visit(orderedList.children)
)
.render()
}
mutating func visitUnorderedList(
_ unorderedList: UnorderedList
) -> Result {
HTML(name: "ul", contents: visit(unorderedList.children)).render()
}
mutating func visitInlineCode(
_ inlineCode: InlineCode
) -> Result {
HTML(
name: "code",
contents: inlineCode.code.escapeAngleBrackets()
)
.render()
}
mutating func visitEmphasis(
_ emphasis: Emphasis
) -> Result {
HTML(name: "em", contents: visit(emphasis.children)).render()
}
mutating func visitStrong(
_ strong: Strong
) -> Result {
HTML(name: "strong", contents: visit(strong.children)).render()
}
mutating func visitStrikethrough(
_ strikethrough: Strikethrough
) -> Result {
HTML(name: "s", contents: visit(strikethrough.children)).render()
}
mutating func visitParagraph(
_ paragraph: Paragraph
) -> Result {
let filterBlocks =
customBlockDirectives
.filter { $0.removesChildParagraph ?? false }
.map(\.name)
if let block = paragraph.parent as? BlockDirective,
filterBlocks.contains(block.name.lowercased())
{
return visit(paragraph.children)
}
/// if the parent is a list element, we don't need to render the p tag
if paragraph.isInsideList {
return visit(paragraph.children)
}
return HTML(name: "p", contents: visit(paragraph.children)).render()
}
mutating func visitBlockQuote(
_ blockQuote: BlockQuote
) -> Result {
var paragraphCount = 0
var otherCount = 0
var type: String?
var dropCount = 0
for i in blockQuote.children {
if let p = i as? Paragraph {
paragraphCount += 1
let text = p.plainText.lowercased()
typeLoop: for (typeValue, prefixes) in paragraphStyles {
for prefix in prefixes {
let fullPrefix = "\(prefix): ".lowercased()
if text.hasPrefix(fullPrefix) {
type = typeValue
dropCount = fullPrefix.count
break typeLoop
}
}
}
}
else {
otherCount += 1
}
}
guard let type, otherCount == 0, paragraphCount == 1 else {
return HTML(
name: "blockquote",
contents: visit(blockQuote.children)
)
.render()
}
let paragraph = visit(blockQuote.children)
let pTagCount = 3
let contents =
paragraph.prefix(pTagCount)
+ paragraph.dropFirst(pTagCount).dropFirst(dropCount)
return HTML(
name: "blockquote",
attributes: [
.init(key: "class", value: type)
],
contents: String(contents)
)
.render()
}
mutating func visitCodeBlock(
_ codeBlock: CodeBlock
) -> Result {
var attributes: [HTML.Attribute] = []
if let language = codeBlock.language {
attributes.append(
.init(
key: "class",
value: "language-\(language.lowercased())"
)
)
}
let code = HTML(
name: "code",
attributes: attributes,
contents: codeBlock.code
.escapeAngleBrackets()
.replacing(
[
#"/*!*/"#: #"<span class="highlight">"#,
#"/*.*/"#: "</span>",
]
)
)
.render()
return HTML(name: "pre", contents: code).render()
}
mutating func visitHeading(
_ heading: Heading
) -> Result {
var attributes: [HTML.Attribute] = []
if [2, 3].contains(heading.level) {
let fragment = heading.plainText.lowercased().slugify()
let id = HTML.Attribute(key: "id", value: "\(fragment)")
attributes.append(id)
}
return HTML(
name: "h\(heading.level)",
attributes: attributes,
contents: visit(heading.children)
)
.render()
}
mutating func visitLink(
_ link: Link
) -> Result {
var attributes: [HTML.Attribute] = []
if let destination = link.destination {
let anchorPrefix = "#[name]"
if destination.hasPrefix(anchorPrefix) {
attributes.append(
.init(
key: "name",
value: String(destination.dropFirst(anchorPrefix.count))
)
)
}
else {
var hrefDestination = destination
if destination.hasPrefix("/") {
hrefDestination =
"\(baseURL.ensureTrailingSlash())\(destination.dropFirst())"
}
attributes.append(
.init(
key: "href",
value: hrefDestination
)
)
}
if !destination.hasPrefix("."),
!destination.hasPrefix("/"),
!destination.hasPrefix("#")
{
attributes.append(
.init(
key: "target",
value: "_blank"
)
)
}
}
return HTML(
name: "a",
attributes: attributes,
contents: visit(link.children)
)
.render()
}
mutating func visitImage(_ image: Image) -> Result {
guard let source = image.source, !source.isEmpty else {
return ""
}
let imagePath = source.resolveAsset(
baseURL: baseURL,
assetsPath: assetsPath,
slug: slug
)
var attributes: [HTML.Attribute] = [
.init(key: "src", value: imagePath),
.init(key: "alt", value: image.plainText),
]
if let title = image.title {
attributes.append(
.init(key: "title", value: title)
)
}
return HTML(
name: "img",
type: .short,
attributes: attributes
)
.render()
}
// MARK: - table
mutating func visitTable(
_ table: Table
) -> Result {
HTML(name: "table", contents: visit(table.children)).render()
}
mutating func visitTableHead(
_ tableHead: Table.Head
) -> Result {
HTML(name: "thead", contents: visit(tableHead.children)).render()
}
mutating func visitTableBody(
_ tableBody: Table.Body
) -> Result {
HTML(name: "tbody", contents: visit(tableBody.children)).render()
}
mutating func visitTableRow(
_ tableRow: Table.Row
) -> Result {
HTML(name: "tr", contents: visit(tableRow.children)).render()
}
mutating func visitTableCell(
_ tableCell: Table.Cell
) -> Result {
HTML(name: "td", contents: visit(tableCell.children)).render()
}
// MARK: - custom block directives
mutating func visitBlockDirective(
_ blockDirective: BlockDirective
) -> Result {
var parseErrors = [DirectiveArgumentText.ParseError]()
var arguments: [DirectiveArgument] = []
let blockName = blockDirective.name.lowercased()
if !blockDirective.argumentText.isEmpty {
arguments = blockDirective.argumentText.parseNameValueArguments(
parseErrors: &parseErrors
)
}
let block = customBlockDirectives.first {
$0.name.lowercased() == blockName.lowercased()
}
guard let block else {
logger.warning(
"Unrecognized block directive: `\(blockName)`",
metadata: [
"name": .string(blockName)
]
)
return ""
}
guard parseErrors.isEmpty else {
let errors =
parseErrors.map { error -> String in
switch error {
case let .duplicateArgument(name, _, _):
return "Duplicate argument: `\(name)`."
case let .missingExpectedCharacter(char, _):
return "Misisng expected character: `\(char)`."
case let .unexpectedCharacter(char, _):
return "Unexpected character: `\(char)`."
}
}
.joined(separator: ", ")
logger.warning(
"\(errors)",
metadata: [
"name": .string(blockName)
]
)
return ""
}
var parameters: [String: String] = [:]
for p in block.parameters ?? [] {
if p.required ?? false {
if let v = arguments.getFirstValueBy(key: p.label) {
parameters[p.label] = v
}
else {
logger.warning(
"Parameter `\(p.label)` for `\(block.name)` is required.",
metadata: [
"name": .string(blockName)
]
)
}
}
else {
let v =
arguments.getFirstValueBy(key: p.label) ?? p.default ?? ""
parameters[p.label] = v
}
}
let templateParams = parameters.mapKeys { "{{\($0)}}" }
if let parent = block.requiresParentDirective, !parent.isEmpty {
guard
let p = blockDirective.parent as? BlockDirective,
p.name.lowercased() == parent.lowercased()
else {
logger.warning(
"Block directive `\(block.name)` requires parent block `\(parent)`",
metadata: [
"name": .string(blockName)
]
)
return ""
}
}
if let output = block.output {
var contents = ""
for child in blockDirective.children {
contents += visit(child)
}
var params = templateParams
params["{{contents}}"] = contents
return output.replacing(params)
}
if let name = block.tag {
let attributes: [HTML.Attribute] =
block.attributes?
.map { a in
.init(
key: a.name,
value: a.value.replacing(templateParams)
)
} ?? []
return HTML(
name: name,
attributes: attributes,
contents: visit(blockDirective.children)
)
.render()
}
return ""
}
}
================================================
FILE: Sources/ToucanMarkdown/Markdown/MarkdownBlockDirective.swift
================================================
//
// MarkdownBlockDirective.swift
// Toucan
//
// Created by Tibor Bödecs on 2025. 02. 19..
//
/// A representation of a custom block directive in Markdown, used for extending Markdown syntax with special tags or behaviors.
public struct MarkdownBlockDirective: Codable, Equatable {
/// Defines a configurable parameter for a directive, which may be required and have a default value.
public struct Parameter: Codable, Equatable {
/// The label of the parameter.
public var label: String
/// Indicates whether the parameter is required. Defaults to `nil` (optional).
public var required: Bool?
/// A default value for the parameter, used if it is not explicitly specified in the directive.
public var `default`: String?
/// Initializes a `Parameter` for a directive.
///
/// - Parameters:
/// - label: The name of the parameter.
/// - isRequired: Indicates if the parameter must be provided.
/// - defaultValue: A fallback value if none is provided.
public init(
label: String,
isRequired: Bool? = nil,
defaultValue: String? = nil
) {
self.label = label
self.required = isRequired
self.default = defaultValue
}
}
/// Represents a static HTML attribute that will be rendered on the directive's HTML tag.
public struct Attribute: Codable, Equatable {
/// The name of the HTML attribute (e.g., `class`, `id`).
public var name: String
/// The corresponding value of the attribute.
public var value: String
/// Initializes an `Attribute` for the rendered directive HTML tag.
///
/// - Parameters:
/// - name: The attribute key.
/// - value: The attribute value.
public init(
name: String,
value: String
) {
self.name = name
self.value = value
}
}
/// The name of the directive (e.g., `"note"`, `"warning"`, `"info"`).
public var name: String
/// A list of supported parameters for the directive.
public var parameters: [Parameter]?
/// If specified, this directive must appear within another directive of the given name.
public var requiresParentDirective: String?
/// Indicates whether child paragraphs should be removed from the HTML output. Defaults to `nil`.
public var removesChildParagraph: Bool?
/// The HTML tag to render (e.g., `"div"`, `"section"`, `"aside"`).
public var tag: String?
/// Static attributes to apply to the rendered HTML tag.
public var attributes: [Attribute]?
/// Custom output HTML string that overrides default rendering behavior, if provided.
public var output: String?
/// Initializes a `MarkdownBlockDirective`.
///
/// - Parameters:
/// - name: The directive's name.
/// - parameters: Optional list of accepted parameters.
/// - requiresParentDirective: Name of a parent directive this one must reside within.
/// - removesChildParagraph: Whether to exclude child `<p>` tags during rendering.
/// - tag: HTML tag to be generated.
/// - attributes: HTML attributes to apply.
/// - output: Optional custom HTML output template.
public init(
name: String,
parameters: [Parameter]? = nil,
requiresParentDirective: String? = nil,
removesChildParagraph: Bool? = nil,
tag: String? = nil,
attributes: [Attribute]? = nil,
output: String? = nil
) {
self.name = name
self.parameters = parameters
self.requiresParentDirective = requiresParentDirective
self.removesChildParagraph = removesChildParagraph
self.tag = tag
self.attributes = attributes
self.output = output
}
}
================================================
FILE: Sources/ToucanMarkdown/Markdown/MarkdownToHTMLRenderer.swift
================================================
//
// MarkdownToHTMLRenderer.swift
// Toucan
//
// Created by Tibor Bödecs on 2025. 02. 19..
//
import Logging
import Markdown
import ToucanCore
/// A renderer that converts Markdown text to HTML, with support for custom block directives and paragraph styling.
public struct MarkdownToHTMLRenderer {
/// Custom block directives to extend Markdown syntax.
public let customBlockDirectives: [MarkdownBlockDirective]
/// A collection of paragraph styles.
public let paragraphStyles: [String: [String]]
/// Logger instance
public let logger: Logger
/// Initializes a `MarkdownToHTMLRenderer`.
///
/// - Parameters:
/// - customBlockDirectives: A list of custom Markdown block directives to parse during rendering.
/// - paragraphStyles: The paragraph styles configuration for styling rendered HTML.
/// - logger: A logger instance for logging. Defaults to a logger labeled "MarkdownToHTMLRenderer".
public init(
customBlockDirectives: [MarkdownBlockDirective] = [],
paragraphStyles: [String: [String]] = [:],
logger: Logger = .subsystem("markdown-to-html-renderer")
) {
self.customBlockDirectives = customBlockDirectives
self.paragraphStyles = paragraphStyles
self.logger = logger
}
// MARK: - render api
/// Renders the provided Markdown string to an HTML string.
///
/// - Parameters:
/// - markdown: The input Markdown text to render.
/// - slug: A slug identifier used for generating.
/// - assetsPath: The path to the assets folder used for resource resolution.
/// - baseURL: The base URL used to resolve relative links within the Markdown.
///
/// - Returns: A fully rendered HTML string.
public func renderHTML(
markdown: String,
slug: String,
assetsPath: String,
baseURL: String
) -> String {
// Create a Markdown document, enabling block directives if any are provided.
let document = Document(
parsing: markdown,
options: !customBlockDirectives.isEmpty
? [.parseBlockDirectives] : []
)
// Initialize the HTML visitor with the current configuration.
var htmlVisitor = HTMLVisitor(
blockDirectives: customBlockDirectives,
paragraphStyles: paragraphStyles,
slug: slug,
assetsPath: assetsPath,
baseURL: baseURL
)
// Generate HTML by visiting the document tree.
return htmlVisitor.visitDocument(document)
}
}
================================================
FILE: Sources/ToucanMarkdown/MarkdownRenderer.swift
================================================
//
// MarkdownRenderer.swift
// Toucan
//
// Created by Tibor Bödecs on 2025. 02. 20..
//
import Logging
import ToucanCore
/// A comprehensive content processing engine that renders Markdown content to HTML,
/// applies transformations, computes reading time, and generates an outline structure.
public struct MarkdownRenderer {
/// Holds all the settings required for rendering and processing content.
public struct Configuration {
/// Configuration specific to Markdown processing.
public struct Markdown {
/// Custom block directives to extend the Markdown grammar.
public var customBlockDirectives: [MarkdownBlockDirective]
//
/// Initializes a Markdown configuration.
public init(
customBlockDirectives: [MarkdownBlockDirective]
) {
self.customBlockDirectives = customBlockDirectives
}
}
/// Configuration for outlining logic, such as which heading levels to parse.
public struct Outline {
/// Which heading levels to include in the parsed outline.
public var levels: [Int]
//
/// Initializes an Outline configuration.
public init(
levels: [Int]
) {
self.levels = levels
}
}
/// Configuration for estimating reading time.
public struct ReadingTime {
/// Estimated words per minute reading speed.
public var wordsPerMinute: Int
//
/// Initializes a ReadingTime configuration.
public init(
wordsPerMinute: Int
) {
self.wordsPerMinute = wordsPerMinute
}
}
/// Markdown-specific rendering options.
public var markdown: Markdown
/// Outline-parsing preferences.
public var outline: Outline
/// Reading time calculation preferences.
public var readingTime: ReadingTime
/// Optional transformation pipeline to apply pre-processing on the input.
public var transformerPipeline: TransformerPipeline?
/// Paragraph styles for customizing the HTML rendering.
public var paragraphStyles: [String: [String]]
/// Initializes a new rendering configuration.
///
/// - Parameters:
/// - markdown: Markdown rendering configuration.
/// - outline: Outline extraction preferences.
/// - readingTime: Reading time estimation settings.
/// - transformerPipeline: Optional content transformation pipeline.
/// - paragraphStyles: Block-level style customization for HTML rendering.
public init(
markdown: Markdown,
outline: Outline,
readingTime: ReadingTime,
transformerPipeline: TransformerPipeline?,
paragraphStyles: [String: [String]]
) {
self.markdown = markdown
self.outline = outline
self.readingTime = readingTime
self.transformerPipeline = transformerPipeline
self.paragraphStyles = paragraphStyles
}
}
/// Final output of the rendering pipeline.
public struct Output {
/// The fully rendered HTML output.
public var html: String
/// Estimated reading time in minutes.
public var readingTime: Int
/// A hierarchical structure representing the document's headings.
public var outline: [Outline]
}
/// Configuration for rendering, including markdown styles, outline levels, and transformation settings.
public var configuration: Configuration
/// Responsible for converting Markdown into HTML with support for custom directives and styling.
public var markdownToHTMLRenderer: MarkdownToHTMLRenderer
/// Parses the rendered HTML to build a heading outline (used for TOC or navigation).
public var outlineParser: OutlineParser
/// Calculates the estimated reading time for a given HTML or Markdown document.
public var readingTimeCalculator: ReadingTimeCalculator
/// Logger for diagnostics and error reporting during rendering.
public var logger: Logger
/// Creates a new `ContentRenderer` instance with the provided configuration, file manager, and logger.
///
/// - Parameters:
/// - configuration: Rendering configuration including markdown, outline, and reading time options.
/// - logger: Optional logger for tracking events and issues.
public init(
configuration: Configuration,
logger: Logger = .subsystem("content-renderer")
) {
self.configuration = configuration
self.markdownToHTMLRenderer = MarkdownToHTMLRenderer(
customBlockDirectives: configuration.markdown.customBlockDirectives,
paragraphStyles: configuration.paragraphStyles
)
self.outlineParser = OutlineParser(
levels: configuration.outline.levels
)
self.readingTimeCalculator = ReadingTimeCalculator(
wordsPerMinute: configuration.readingTime.wordsPerMinute
)
self.logger = logger
}
/// Processes the input Markdown content, optionally transforms it, renders it as HTML,
/// calculates reading time, and generates an outline.
///
/// - Parameters:
/// - content: The raw Markdown content to process.
/// - typeAwareID: A unique identifier used for transformation and rendering context.
/// - slug: The slug of the content.
/// - assetsPath: Path to associated assets (e.g., images or includes).
/// - baseURL: The base URL for resolving relative paths or links.
///
/// - Returns: A structured `Output` containing HTML, reading time, and outline.
public func render(
content: String,
typeAwareID: String,
slug: String,
assetsPath: String,
baseURL: String
) -> Output {
var finalHtml = content
var shouldRenderMarkdown = true
// Step 1: Run transformer pipeline, if defined and non-empty.
if let transformerPipeline = configuration.transformerPipeline {
if !transformerPipeline.run.isEmpty {
shouldRenderMarkdown = transformerPipeline.isMarkdownResult
let executor = TransformerExecutor(
pipeline: transformerPipeline
)
do {
finalHtml = try executor.transform(
contents: finalHtml,
id: typeAwareID,
slug: slug
)
}
catch {
logger.error("\(String(describing: error))")
}
}
else {
logger.warning("Empty transformer pipeline.")
}
}
// Step 2: If the transformer output isn't already HTML, render Markdown to HTML.
if shouldRenderMarkdown {
finalHtml = markdownToHTMLRenderer.renderHTML(
markdown: content,
slug: slug,
assetsPath: assetsPath,
baseURL: baseURL
)
}
// Step 3: Calculate reading time and parse outline from HTML.
let readingTime = readingTimeCalculator.calculate(for: finalHtml)
let outline = outlineParser.parseHTML(finalHtml)
return .init(
html: finalHtml,
readingTime: readingTime,
outline: outline
)
}
}
================================================
FILE: Sources/ToucanMarkdown/Outline/Outline.swift
================================================
//
// Outline.swift
// Toucan
//
// Created by Tibor Bödecs on 2025. 04. 17..
//
/// A hierarchical representation of an outline element, used for
/// structuring headings or sections in a document or interface.
public struct Outline: Equatable, Codable {
/// The depth level of the outline node (e.g., 1 for top-level, 2 for a subheading, etc.).
public var level: Int
/// The display text of the outline entry, such as a heading title.
public var text: String
/// An optional fragment identifier that can be used for navigation (e.g., URL anchors).
public var fragment: String?
/// A list of child outlines, representing nested structure under this node.
public var children: [Outline]
/// Initializes a new `Outline` instance.
///
/// - Parameters:
/// - level: The heading level of the outline (e.g., 1 for `h1`, 2 for `h2`, etc.).
/// - text: The display text for this outline item.
/// - fragment: An optional anchor or link target associated with this item.
/// - children: A list of nested `Outline` elements under this item.
public init(
level: Int,
text: String,
fragment: String? = nil,
children: [Outline] = []
) {
self.level = level
self.text = text
self.fragment = fragment
self.children = children
}
}
================================================
FILE: Sources/ToucanMarkdown/Outline/OutlineParser.swift
================================================
//
// OutlineParser.swift
// Toucan
//
// Created by Viasz-Kádi Ferenc on 2024. 10. 14..
//
import Logging
import SwiftSoup
import ToucanCore
/// A parser that extracts heading elements (`<h1>` to `<h6>`) from HTML and converts them into a structured outline.
public struct OutlineParser {
/// The heading levels (e.g., `[1, 2, 3]` for `<h1>`, `<h2>`, and `<h3>`) to include in the outline.
public var levels: [Int]
/// Logger instance
public var logger: Logger
/// Initializes an `OutlineParser` with optional levels and a logger.
///
/// - Parameters:
/// - levels: Heading levels to extract from the HTML. Must be between 1 and 6. Defaults to all (`[1, 2, 3, 4, 5, 6]`).
/// - logger: A `Logger` instance for capturing logs. Defaults to a logger labeled "OutlineParser".
public init(
levels: [Int] = [1, 2, 3, 4, 5, 6],
logger: Logger = .subsystem("outline-parser")
) {
// Ensure levels are within the valid range of HTML headings.
precondition(
levels.allSatisfy { 1...6 ~= $0 },
"Values must be between 1 and 6."
)
self.levels = levels
self.logger = logger
}
/// Converts a single SwiftSoup element into an `Outline` if it corresponds to a valid heading.
///
/// - Parameter element: A SwiftSoup `Element` representing a heading node.
/// - Returns: An `Outline` instance if the element is a valid heading, otherwise `nil`.
/// - Throws: An error if parsing the element fails.
func createToC(
from element: SwiftSoup.Element
) throws -> Outline? {
let text = try element.text()
let nodeName = element.nodeName()
guard
nodeName.count > 1,
let rawLevel = nodeName.last,
let level = Int(String(rawLevel)),
(1...6).contains(level)
else {
return nil
}
var fragment: String?
let id = try element.attr("id")
if !id.isEmpty {
fragment = id
}
return .init(
level: level,
text: text,
fragment: fragment
)
}
/// Parses the given HTML string and returns a flat list of `Outline` items corresponding to the specified heading levels.
///
/// - Parameter html: A string of HTML content.
/// - Returns: An array of `Outline` instances representing the headings found.
public func parseHTML(
_ html: String
) -> [Outline] {
do {
// Parse HTML content into a SwiftSoup document.
let document = try SwiftSoup.parse(html)
// Build a CSS selector for the specified heading levels (e.g., "h1, h2, h3").
let tagSelector = levels.map { "h\($0)" }.joined(separator: ", ")
// Select and process matching heading elements.
let headings = try document.select(tagSelector)
return try headings.compactMap { try createToC(from: $0) }
}
catch let Exception.Error(type, message) {
logger.error("\(type) - \(message)")
return []
}
catch {
logger.error("\(error.localizedDescription)")
return []
}
}
}
================================================
FILE: Sources/ToucanMarkdown/ReadingTime/ReadingTimeCalculator.swift
================================================
//
// ReadingTimeCalculator.swift
// Toucan
//
// Created by Viasz-Kádi Ferenc on 2024. 10. 15..
//
import Logging
import ToucanCore
/// A utility to estimate the reading time of a given string of text based on words per minute.
public struct ReadingTimeCalculator {
/// The number of words assumed to be read per minute.
public var wordsPerMinute: Int
/// Logger instance
public var logger: Logger
/// Initializes a new instance of `ReadingTimeCalculator`.
///
/// - Parameters:
/// - wordsPerMinute: The number of words a person can read per minute. Defaults to 238.
/// - logger: A `Logger` instance for logging internal operations. Defaults to a logger labeled "ReadingTimeCalculator".
public init(
wordsPerMinute: Int = 238,
logger: Logger = .subsystem("reading-time-calculator")
) {
self.wordsPerMinute = wordsPerMinute
self.logger = logger
}
/// Calculates the estimated reading time for a given string.
///
/// - Parameter string: The input text to estimate reading time for.
/// - Returns: An estimated reading time in minutes. Returns at least 1 minute.
public func calculate(
for string: String
) -> Int {
max(string.split(separator: " ").count / wordsPerMinute, 1)
}
}
================================================
FILE: Sources/ToucanMarkdown/Transformers/ContentTransformer.swift
================================================
//
// ContentTransformer.swift
// Toucan
//
// Created by Tibor Bödecs on 2025. 02. 21..
//
/// Represents a content transformer command used in a transformation pipeline.
public struct ContentTransformer {
/// The directory path where the executable is located.
/// Defaults to `"/usr/local/bin"` if not explicitly specified.
public var path: String
/// The name of the executable or script to run.
public var name: String
/// Initializes a new `ContentTransformer` with an optional path and required name.
///
/// - Parameters:
/// - path: The directory path to the executable. Defaults to `"/usr/local/bin"`.
/// - name: The name of the command-line executable or script.
public init(
path: String = "/usr/local/bin",
name: String
) {
self.path = path
self.name = name
}
}
================================================
FILE: Sources/ToucanMarkdown/Transformers/TransformerExecutor.swift
================================================
//
// TransformerExecutor.swift
// Toucan
//
// Created by Viasz-Kádi Ferenc on 2024. 10. 15..
//
import Foundation
import Logging
import SwiftCommand
import ToucanCore
/// Executes a sequence of shell-based transformation commands defined in a `TransformerPipeline`,
/// allowing content to be programmatically modified.
public struct TransformerExecutor {
/// The transformation pipeline consisting of commands to execute.
public var pipeline: TransformerPipeline
/// File manager utility for file system interactions, including temp files and cleanup.
public var fileManager: FileManager
/// Logger instance.
public var logger: Logger
/// Initializes a `TransformerExecutor` with a transformation pipeline and file manager.
///
/// - Parameters:
/// - pipeline: A sequence of external commands to run for transformation.
/// - fileManager: A file manager abstraction for working with files.
/// - logger: A logger for capturing stdout, stderr, and errors.
public init(
pipeline: TransformerPipeline,
fileManager: FileManager = .default,
logger: Logger = .subsystem("transformer-executor")
) {
self.pipeline = pipeline
self.fileManager = fileManager
self.logger = logger
}
/// Transforms the given content string using the defined pipeline.
///
/// This function:
/// - Saves the content to a temporary file.
/// - Executes each command in the pipeline sequentially, modifying the file in place.
/// - Captures and logs output and errors.
/// - Returns the final transformed content.
///
/// - Parameters:
/// - contents: The raw content to be transformed.
/// - id: An identifier used to pass context to the commands.
/// - slug: The slug of the content.
///
/// - Throws: Rethrows any error encountered during reading, writing, or transformation.
/// - Returns: The final transformed content string.
public func transform(
contents: String,
id: String,
slug: String
) throws -> String {
// Step 1: Write the content to a temp file
let tempDirectoryURL = fileManager.temporaryDirectory
let fileName = UUID().uuidString
let fileURL = tempDirectoryURL.appendingPathComponent(fileName)
try contents.write(to: fileURL, atomically: true, encoding: .utf8)
// Step 2: Run each command in the transformation pipeline
for command in pipeline.run {
do {
let arguments: [String] = [
"--id", id,
"--file", fileURL.path,
"--slug", slug,
]
let commandURL = URL(fileURLWithPath: command.path)
.appendingPathComponent(command.name)
let command = Command(executablePath: .init(commandURL.path()))
.addArguments(arguments)
let result = try command.waitForOutput()
// Log output and errors
if !result.stdout.isEmpty {
logger.debug("\(result)")
}
if let err = result.stderr, !err.isEmpty {
logger.error("\(err)")
}
}
catch {
logger.error("\(error))")
}
}
// Step 3: Read the transformed contents, clean up, and return
do {
let finalContents = try String(
contentsOf: fileURL,
encoding: .utf8
)
try fileManager.removeItem(at: fileURL)
return finalContents
}
catch {
// Ensure cleanup is still performed
try fileManager.removeItem(at: fileURL)
throw error
}
}
}
================================================
FILE: Sources/ToucanMarkdown/Transformers/TransformerPipeline.swift
================================================
//
// TransformerPipeline.swift
// Toucan
//
// Created by Tibor Bödecs on 2025. 02. 21..
//
/// Represents a sequence of content transformers to run before rendering,
/// along with an indicator of whether the final result is Markdown.
public struct TransformerPipeline {
/// An ordered list of transformers (external commands or scripts) to execute.
///
/// Each `ContentTransformer` represents an individual transformation step.
public var run: [ContentTransformer]
/// Indicates whether the final output from this pipeline is expected to be Markdown.
///
/// If `false`, the renderer may treat the output as already-formatted HTML or another format.
public var isMarkdownResult: Bool
/// Initializes a new `TransformerPipeline`.
///
/// - Parameters:
/// - run: An array of `ContentTransformer` instances to execute.
/// - isMarkdownResult: A flag indicating whether the final output is Markdown. Defaults to `true`.
public init(
run: [ContentTransformer] = [],
isMarkdownResult: Bool = true
) {
self.run = run
self.isMarkdownResult = isMarkdownResult
}
}
================================================
FILE: Sources/ToucanSDK/Behaviors/Behavior.swift
================================================
//
// Behavior.swift
// Toucan
//
// Created by Tibor Bödecs on 2025. 06. 16..
//
import struct Foundation.URL
/// A protocol that defines a behavior with a unique identifier
/// and an operation that runs on a given file URL.
protocol Behavior {
/// A unique identifier for the behavior.
static var id: String { get }
/// Executes the behavior with the given file URL.
///
/// - Parameter fileURL: The URL of the file to process.
/// - Returns: A `String` result of the behavior.
/// - Throws: An error if the behavior fails.
func run(fileURL: URL) throws -> String
}
================================================
FILE: Sources/ToucanSDK/Behaviors/CompileSASSBehavior.swift
================================================
//
// CompileSASSBehavior.swift
// Toucan
//
// Created by Tibor Bödecs on 2025. 05. 12..
//
import DartSass
import Foundation
struct CompileSASSBehavior: Behavior {
static let id = "compile-sass"
var compiler: Compiler
init() throws {
self.compiler = try .init()
}
/// NOTE: This is horrible... but we can live with it for a while :)
private func unsafeSyncCompile(fileURL: URL) -> String {
final class Enclosure: @unchecked Sendable {
var value: CompilerResults!
}
let semaphore = DispatchSemaphore(value: 0)
let enclosure = Enclosure()
Task {
do {
enclosure.value =
try await compiler.compile(
fileURL: fileURL
)
}
catch {
fatalError("\(error) - \(fileURL.path())")
}
semaphore.signal()
}
semaphore.wait()
return enclosure.value.css
}
func run(fileURL: URL) throws -> String {
let css = unsafeSyncCompile(fileURL: fileURL)
return css
}
}
================================================
FILE: Sources/ToucanSDK/Behaviors/MinifyCSSBehavior.swift
================================================
//
// MinifyCSSBehavior.swift
// Toucan
//
// Created by Tibor Bödecs on 2025. 05. 12..
//
import Foundation
import SwiftCSSParser
struct MinifyCSSBehavior: Behavior {
static let id = "minify-css"
func run(fileURL: URL) throws -> String {
let src = try String(
contentsOf: fileURL,
encoding: .utf8
)
let stylesheet = try Stylesheet.parse(from: src)
return stylesheet.minified()
}
}
================================================
FILE: Sources/ToucanSDK/Content/Content+Query.swift
================================================
//
// Content+Query.swift
// Toucan
//
// Created by Tibor Bödecs on 2025. 01. 31..
//
import Foundation
import ToucanSource
import Logging
public extension Content {
/// Flattens the content's core properties, relations, and metadata into a single dictionary
/// for use in filtering, querying, or templating contexts.
///
/// - Includes:
/// - All `properties` as defined in the content type
/// - Resolved `relations`, where:
/// - `.one` types return a single identifier (or an empty array if unresolved)
/// - `.many` types return an array of identifiers
/// - Additional metadata:
/// - `"id"`: The content's unique identifier
/// - `"slug"`: The slug string used for URLs
/// - `"lastUpdate"`: Last modification timestamp of the content
/// - `"iterator"`: Boolean flag indicating if this content is an iterator item
///
/// - Returns: A `[String: AnyCodable]` dictionary representing queryable fields.
var queryFields: [String: AnyCodable] {
var fields = properties
// Flatten relational fields by type
for (key, relation) in relations {
switch relation.type {
case .one:
if relation.identifiers.isEmpty {
// Default to empty array if no target
fields[key] = .init([])
}
else {
fields[key] = .init(relation.identifiers[0]) // Single ID
}
case .many:
fields[key] = .init(relation.identifiers) // Array of IDs
}
}
// Append metadata fields
fields[SystemPropertyKeys.id.rawValue] = .init(typeAwareID)
fields[SystemPropertyKeys.lastUpdate.rawValue] = .init(
rawValue.lastModificationDate
)
fields[SystemPropertyKeys.slug.rawValue] = .init(slug.value)
fields[RootContextKeys.iterator.rawValue] = .init(isIterator)
return fields
}
}
public extension [Content] {
/// Executes a `Query` against the current content collection, applying filtering,
/// sorting, and pagination.
///
/// - Parameters:
/// - query: The `Query` object containing filtering, ordering, and limit logic.
/// - now: The current timestamp used for time-based filtering.
/// - logger: A `Logger` instance for capturing logs.
/// - Returns: A filtered, sorted, and paginated array of `Content` items.
func run(
query: Query,
now: TimeInterval,
logger: Logger
) -> [Content] {
let contents = filter { query.contentType == $0.type.id }
return filter(
contents: contents,
using: query.resolveFilterParameters(
with: [
"date.now": .init(now)
]
),
logger: logger
)
}
/// Filters, sorts, and slices the given content array based on a query.
private func filter(
contents: [Content],
using query: Query,
logger: Logger
) -> [Content] {
var filteredContents = contents.filter { element in
evaluate(condition: query.filter, with: element.queryFields)
}
for order in query.orderBy.reversed() {
filteredContents.sort { a, b in
let propertyForOrderKey: (Content) -> AnyCodable? = { item in
guard let value = item.properties[order.key] else {
logger.warning(
"Missing order property key: `\(order.key)`.",
metadata: [
"slug": .string(item.slug.value),
"contentType": .string(query.contentType),
]
)
return nil
}
return value
}
guard
let valueA = propertyForOrderKey(a),
let valueB = propertyForOrderKey(b)
else {
return false
}
return compare(
valueA,
valueB,
ascending: order.direction == .asc
)
}
}
if let offset = query.offset {
filteredContents = Array(filteredContents.dropFirst(offset))
}
if let limit = query.limit {
filteredContents = Array(filteredContents.prefix(limit))
}
return filteredContents
}
/// Recursively evaluates a `Condition` tree against a set of content fields.
private func evaluate(
condition: Condition?,
with props: [String: AnyCodable]
) -> Bool {
guard let condition else { return true }
switch condition {
case let .field(key, `operator`, value):
guard let fieldValue = props[key] else { return false }
return evaluateField(
fieldValue: fieldValue,
operator: `operator`,
value: value
)
case let .and(conditions):
return conditions.allSatisfy {
evaluate(condition: $0, with: props)
}
case let .or(conditions):
return conditions.contains {
evaluate(condition: $0, with: props)
}
}
}
/// Compares two values for equality, supporting multiple types.
private func equals(_ valueA: AnyCodable, _ valueB: AnyCodable) -> Bool {
if let a = valueA.value(as: Bool.self),
let b = valueB.value(as: Bool.self)
{
return a == b
}
if let a = valueA.value(as: Int.self),
let b = valueB.value(as: Int.self)
{
return a == b
}
if let a = valueA.value(as: Double.self),
let b = valueB.value(as: Double.self)
{
return a == b
}
if let a = valueA.value(as: String.self),
let b = valueB.value(as: String.self)
{
return a == b
}
return false
}
/// Performs numeric or string comparison between two values, with optional inclusiveness.
private func compare(
_ valueA: AnyCodable,
_ valueB: AnyCodable,
ascending: Bool,
isInclusive: Bool = false
) -> Bool {
if let a = valueA.value(as: Int.self),
let b = valueB.value(as: Int.self)
{
return isInclusive
? (ascending ? a <= b : a >= b) : (ascending ? a < b : a > b)
}
if let a = valueA.value(as: Double.self),
let b = valueB.value(as: Double.self)
{
return isInclusive
? (ascending ? a <= b : a >= b) : (ascending ? a < b : a > b)
}
if let a = valueA.value(as: String.self),
let b = valueB.value(as: String.self)
{
return isInclusive
? (ascending ? a <= b : a >= b) : (ascending ? a < b : a > b)
}
return false
}
/// Evaluates a field condition against a value using the provided operator.
private func evaluateField(
fieldValue: AnyCodable,
operator: Operator,
value: AnyCodable
) -> Bool {
switch `operator` {
case .equals: return equals(fieldValue, value)
case .notEquals: return !equals(fieldValue, value)
case .lessThan: return compare(fieldValue, value, ascending: true)
case .greaterThan: return compare(fieldValue, value, ascending: false)
case .lessThanOrEquals:
return compare(
fieldValue,
value,
ascending: true,
isInclusive: true
)
case .greaterThanOrEquals:
return compare(
fieldValue,
value,
ascending: false,
isInclusive: true
)
case .like:
return fieldValue.value(as: String.self)?
.contains(value.value(as: String.self) ?? "") ?? false
case .caseInsensitiveLike:
return fieldValue.value(as: String.self)?
.lowercased()
.contains(value.value(as: String.self)?.lowercased() ?? "")
?? false
case .in:
if let v = fieldValue.value(as: Int.self),
let arr = value.value(as: [Int].self)
{
return arr.contains(v)
}
if let v = fieldValue.value(as: Double.self),
let arr = value.value(as: [Double].self)
{
return arr.contains(v)
}
if let v = fieldValue.value(as: String.self),
let arr = value.value(as: [String].self)
{
return arr.contains(v)
}
return false
case .contains:
if let arr = fieldValue.value(as: [Int].self),
let v = value.value(as: Int.self)
{
return arr.contains(v)
}
if let arr = fieldValue.value(as: [Double].self),
let v = value.value(as: Double.self)
{
return arr.contains(v)
}
if let arr = fieldValue.value(as: [String].self),
let v = value.value(as: String.self)
{
return arr.contains(v)
}
return false
case .matching:
if let arr = fieldValue.value(as: [Int].self),
let other = value.value(as: [Int].self)
{
return !Set(arr).intersection(other).isEmpty
}
if let arr = fieldValue.value(as: [Double].self),
let other = value.value(as: [Double].self)
{
return !Set(arr).intersection(other).isEmpty
}
if let arr = fieldValue.value(as: [String].self),
let other = value.value(as: [String].self)
{
return !Set(arr).intersection(other).isEmpty
}
return false
}
}
}
================================================
FILE: Sources/ToucanSDK/Content/Content.swift
================================================
//
// Content.swift
// Toucan
//
// Created by Tibor Bödecs on 2025. 01. 15..
//
import ToucanSource
/// Represents a unit of structured content, with associated metadata, relationships, and rendering information.
public struct Content {
/// The content type definition that describes structure and expected fields.
public var type: ContentType
/// A globally unique string identifier for this content item.
/// This value remains constant across contexts and is used for persistence or lookup.
public var typeAwareID: String
/// A URL-friendly slug that identifies the content in paths or links.
public var slug: Slug
/// The raw content representation, usually Markdown or HTML source.
public var rawValue: RawContent
/// A dictionary of properties that hold the parsed field values (e.g., title, date, body).
/// Keys are field names as defined in the `ContentType`, and values are dynamically typed.
public var properties: [String: AnyCodable]
/// A dictionary of relations to other content items, keyed by relation name.
/// The relation values may include identifiers or full references depending on usage.
public var relations: [String: RelationValue]
/// Arbitrary user-defined metadata not explicitly declared in the content definition.
/// These are typically useful for extensibility or plugin features.
public var userDefined: [String: AnyCodable]
/// Optional iterator metadata if the content is generated through iteration (e.g., paginated or list item).
public var iteratorInfo: IteratorInfo?
/// A computed flag indicating whether this content instance was generated via iteration.
public var isIterator: Bool { iteratorInfo != nil }
/// Initializes a new `Content` instance.
///
/// - Parameters:
/// - type: Structural schema for this content.
/// - typeAwareID: A unique identifier.
/// - slug: A human-readable URL slug.
/// - rawValue: The unparsed content.
/// - properties: Parsed content fields.
/// - relations: Links to other content.
/// - userDefined: Freeform or plugin-provided metadata.
/// - iteratorInfo: Optional info for repeated or generated content.
public init(
type: ContentType,
typeAwareID: String,
slug: Slug,
rawValue: RawContent,
properties: [String: AnyCodable],
relations: [String: RelationValue],
userDefined: [String: AnyCodable],
iteratorInfo: IteratorInfo?
) {
self.type = type
self.typeAwareID = typeAwareID
self.slug = slug
self.rawValue = rawValue
self.properties = properties
self.relations = relations
self.userDefined = userDefined
self.iteratorInfo = iteratorInfo
}
}
================================================
FILE: Sources/ToucanSDK/Content/ContentResolver.swift
================================================
//
// ContentResolver.swift
// Toucan
//
// Created by Tibor Bödecs on 2025. 05. 21..
//
import Foundation
import Logging
import ToucanCore
import ToucanSerialization
import ToucanSource
private extension Path {
func getTypeAwareIdentifier() -> String {
let newRawPath =
value
.split(separator: "/")
.last
.map(String.init) ?? ""
return Path(newRawPath).trimmingBracketsContent()
}
}
enum ContentResolverError: ToucanError {
case contentType(ContentTypeResolverError)
case missingProperty(String, String)
case missingRelation(String, String)
case invalidProperty(String, String, String)
case invalidSlug(String)
case unknown(Error)
var underlyingErrors: [any Error] {
switch self {
case let .contentType(error):
[error]
case .missingProperty:
[]
case .missingRelation:
[]
case .invalidProperty:
[]
case .invalidSlug:
[]
case let .unknown(error):
[error]
}
}
var logMessage: String {
switch self {
case .contentType(_):
"Content type related error."
case let .missingProperty(name, slug):
"Missing property `\(name)` for content: \(slug)."
case let .missingRelation(name, slug):
"Missing property `\(name)` for content: \(slug)."
case let .invalidProperty(name, value, slug):
"Invalid property `\(name): \(value)` for content: \(slug)."
case let .invalidSlug(slug):
"Invalid slug for content: \(slug)."
case let .unknown(error):
error.localizedDescription
}
}
var userFriendlyMessage: String {
switch self {
case .contentType(_):
"Content type related error."
case let .missingProperty(name, slug):
"Missing property `\(name)` for content: `\(slug)`."
case let .missingRelation(name, slug):
"Missing property `\(name)` for content: `\(slug)`."
case let .invalidProperty(name, value, slug):
"Invalid property `\(name): \(value)` for content: \(slug)."
case let .invalidSlug(slug):
"Invalid slug for content: \(slug)."
case .unknown:
"Unknown content conversion error."
}
}
}
struct ContentResolver {
var contentTypeResolver: ContentTypeResolver
var encoder: ToucanEncoder
var decoder: ToucanDecoder
var dateFormatter: ToucanInputDateFormatter
var logger: Logger
init(
contentTypeResolver: ContentTypeResolver,
encoder: ToucanEncoder,
decoder: ToucanDecoder,
dateFormatter: ToucanInputDateFormatter,
logger: Logger = .subsystem("content-resolver")
) {
self.contentTypeResolver = contentTypeResolver
self.encoder = encoder
self.decoder = decoder
self.dateFormatter = dateFormatter
self.logger = logger
}
private func rewrite(
iteratorID: String,
pageIndex: Int,
_ value: inout String
) {
value = value.replacing([
"{{\(iteratorID)}}": String(pageIndex)
])
}
private func rewrite(
number: Int,
total: Int,
_ array: inout [String: AnyCodable]
) {
for (key, _) in array {
if let stringValue = array[key]?.stringValue() {
array[key] = .init(
replace(
in: stringValue,
number: number,
total: total
)
)
}
}
}
private func replace(
in value: String,
number: Int,
total: Int
) -> String {
value.replacing([
"{{number}}": String(number),
"{{total}}": String(total),
])
}
private func createDictionaryValues(
assetKeys: [String],
array: [String]
) -> [String: AnyCodable] {
var values: [String: AnyCodable] = [:]
for i in 0..<array.count {
values[assetKeys[i]] = .init(array[i])
}
return values
}
private func filterFilePaths(
from paths: [String],
input: Pipeline.Assets.Location
) -> [String] {
paths.filter { filePath in
guard let url = URL(string: filePath) else {
return false
}
let path = url.deletingLastPathComponent().path
let name = url.deletingPathExtension().lastPathComponent
let ext = url.pathExtension
let inputPath = input.path ?? ""
let pathMatches =
inputPath == "*" || inputPath.isEmpty || path == inputPath
let nameMatches =
input.name == "*" || input.name.isEmpty || name == input.name
let extMatches =
input.ext == "*" || input.ext.isEmpty || ext == input.ext
return pathMatches && nameMatches && extMatches
}
}
// MARK: - asset behaviors
private func getNameAndExtension(
from path: String
) -> (name: String, ext: String) {
let safePath = path.split(separator: "/").last.map(String.init) ?? ""
let parts = safePath.split(
separator: ".",
omittingEmptySubsequences: false
)
guard parts.count >= 2 else {
return (String(safePath), "") // No extension
}
let ext = String(parts.last!)
let filename = parts.dropLast().joined(separator: ".")
return (filename, ext)
}
// MARK: - conversion
func convert(
rawContents: [RawContent]
) throws(ContentResolverError) -> [Content] {
do {
return try rawContents.map {
try convert(rawContent: $0)
}
}
catch let error as ContentResolverError {
throw error
}
catch {
throw .unknown(error)
}
}
// MARK: - error helper
func getContentType(
for origin: Origin,
using id: String?
) throws(ContentResolverError) -> ContentType {
do {
return try contentTypeResolver.getContentType(
for: origin,
using: id
)
}
catch {
throw .contentType(error)
}
}
// MARK: - conversion helpers
func convert(
property: Property,
rawValue: AnyCodable?,
forKey key: String,
slug: String
) throws(ContentResolverError) -> AnyCodable? {
let value = rawValue ?? property.defaultValue
switch property.type {
case let .date(config):
guard
let rawDateValue = value?.value(as: String.self)
else {
throw .invalidProperty(
key,
value?.stringValue() ?? "nil",
slug
)
}
guard
let date = dateFormatter.date(
from: rawDateValue,
using: config
)
else {
throw .invalidProperty(
key,
value?.stringValue() ?? "nil",
slug
)
}
return .init(date.timeIntervalSince1970)
default:
return value
}
}
func convert(
rawContent: RawContent
) throws(ContentResolverError) -> Content {
let typeID = rawContent.markdown.frontMatter.string(
SystemPropertyKeys.type.rawValue
)
let contentType = try getContentType(
for: rawContent.origin,
using: typeID
)
var properties: [String: AnyCodable] = [:]
// validate properties
let frontMatter = rawContent.markdown.frontMatter
let missingProperties = contentType.properties
.filter { name, property in
let isRequiredButMissing =
property.required && frontMatter[name] == nil
let hasNoDefaultValue = property.defaultValue?.value == nil
let isNotSystemProperty = !SystemPropertyKeys.allCases
.map { $0.rawValue }
.contains(name)
return isRequiredButMissing && hasNoDefaultValue
&& isNotSystemProperty
}
for name in missingProperties.keys {
throw .missingProperty(name, rawContent.origin.slug)
}
/// validate relations
let missingRelations = contentType.relations.keys.filter {
frontMatter[$0] == nil
}
for name in missingRelations {
throw .missingRelation(name, rawContent.origin.slug)
}
// Extrant `id` from front matter or path or fallback to origin path
var typeAwareID = rawContent.origin.path.getTypeAwareIdentifier()
if let id = rawContent.markdown.frontMatter.string(
SystemPropertyKeys.id.rawValue
) {
typeAwareID = id
}
// Extract `slug` from front matter or fallback to origin slug
var slug: String = rawContent.origin.slug
if let rawSlug = rawContent.markdown.frontMatter.string(
SystemPropertyKeys.slug.rawValue,
allowingEmptyValue: true
) {
guard rawSlug.containsOnlyValidURLCharacters() else {
throw .invalidSlug(rawSlug)
}
slug = rawSlug // .slugify()
}
// Convert schema-defined properties
for (key, property) in contentType.properties.sorted(by: {
$0.key < $1.key
}) {
var rawValue: AnyCodable?
switch key {
case SystemPropertyKeys.id.rawValue:
rawValue = .init(typeAwareID)
case SystemPropertyKeys.lastUpdate.rawValue:
rawValue = .init(rawContent.lastModificationDate)
case SystemPropertyKeys.slug.rawValue:
rawValue = .init(slug)
case SystemPropertyKeys.type.rawValue:
rawValue = .init(typeID)
default:
rawValue = rawContent.markdown.frontMatter[key]
}
properties[key] = try convert(
property: property,
rawValue: rawValue,
forKey: key,
slug: rawContent.origin.slug
)
}
// Convert schema-defined relations
var relations: [String: RelationValue] = [:]
for (key, relation) in contentType.relations.sorted(by: {
$0.key < $1.key
}) {
let rawValue = rawContent.markdown.frontMatter[key]
var identifiers: [String] = []
switch relation.type {
case .one:
if let id = rawValue?.value as? String {
identifiers.append(id)
}
case .many:
if let ids = rawValue?.value as? [String] {
identifiers.append(contentsOf: ids)
}
}
relations[key] = .init(
contentType: relation.references,
type: relation.type,
identifiers: identifiers
)
}
// Filter out reserved keys and schema-mapped fields to extract user-defined fields
let keysToRemove =
SystemPropertyKeys.allCases
.map { $0.rawValue }
+ contentType.properties.keys
+ contentType.relations.keys
var userDefined = rawContent.markdown.frontMatter
for key in keysToRemove {
userDefined.removeValue(forKey: key)
}
logger.trace(
"Converting content",
metadata: [
"type": .string(contentType.id),
"typeAwareID": .string(typeAwareID),
"slug": .string(slug),
"origin": .dictionary(
[
"path": .string(rawContent.origin.path.value),
"slug": .string(rawContent.origin.slug),
]
),
]
)
return .init(
type: contentType,
typeAwareID: typeAwareID,
slug: .init(slug),
rawValue: rawContent,
properties: properties,
relations: relations,
userDefined: userDefined,
iteratorInfo: nil
)
}
// MARK: - filter
/// Applies the filtering rules to the provided content items.
///
/// - Parameters:
/// - filterRules: A dictionary mapping content type identifiers to filtering conditions.
/// - contents: The list of `Content` items to filter.
/// - now: The current timestamp used for time-based filtering.
/// - Returns: A new list containing only the filtered content items.
func apply(
filterRules: [String: Condition],
to contents: [Content],
now: TimeInterval
) -> [Content] {
let groups = Dictionary(grouping: contents, by: { $0.type.id })
var result: [Content] = []
for (id, contents) in groups {
if let condition = filterRules[id] ?? filterRules["*"] {
let items = contents.run(
query: .init(
contentType: id,
filter: condition
),
now: now,
logger: logger
)
result.append(contentsOf: items)
}
else {
result.append(contentsOf: contents)
}
}
return result
}
// MARK: - iterators
func apply(
iterators: [String: Query],
to contents: [Content],
baseURL: String,
now: TimeInterval
) -> [Content] {
var finalContents: [Content] = []
for content in contents {
if let iteratorID = content.slug.extractIteratorID() {
guard
let query = iterators[iteratorID]
else {
continue
}
let countQuery = Query(
contentType: query.contentType,
scope: query.scope,
limit: nil,
offset: nil,
filter: query.filter,
orderBy: query.orderBy
)
let total =
contents.run(query: countQuery, now: now, logger: logger)
.count
let limit = max(1, query.limit ?? 10)
let numberOfPages = (total + limit - 1) / limit
for i in 0..<numberOfPages {
let offset = i * limit
let currentPageIndex = i + 1
var alteredContent = content
rewrite(
iteratorID: iteratorID,
pageIndex: currentPageIndex,
&alteredContent.typeAwareID
)
rewrite(
iteratorID: iteratorID,
pageIndex: currentPageIndex,
&alteredContent.slug.value
)
rewrite(
number: currentPageIndex,
total: numberOfPages,
&alteredContent.properties
)
rewrite(
number: currentPageIndex,
total: numberOfPages,
&alteredContent.userDefined
)
if !alteredContent.rawValue.markdown.contents.isEmpty {
alteredContent.rawValue.markdown.contents = replace(
in: alteredContent.rawValue.markdown.contents,
number: currentPageIndex,
total: numberOfPages
)
}
let links = (0..<numberOfPages)
.map { i in
let pageIndex = i + 1
let permalink = content.slug.permalink(
baseURL: baseURL
)
return IteratorInfo.Link(
number: pageIndex,
permalink: permalink.replacing(
["{{\(iteratorID)}}": String(pageIndex)]
),
isCurrent: pageIndex == currentPageIndex
)
}
let items = contents.run(
query: .init(
contentType: query.contentType,
limit: limit,
offset: offset,
filter: query.filter,
orderBy: query.orderBy
),
now: now,
logger: logger
)
alteredContent.iteratorInfo = .init(
current: currentPageIndex,
total: numberOfPages,
limit: limit,
items: items,
links: links,
scope: query.scope
)
finalContents.append(alteredContent)
}
}
else {
finalContents.append(content)
}
}
return finalContents
}
// MARK: - asset resolution
func apply(
assetProperties: [Pipeline.Assets.Property],
to contents: [Content],
contentsURL: URL,
assetsPath: String,
baseURL: String
) throws -> [Content] {
var results: [Content] = []
for content in contents {
var item: Content = content
for property in assetProperties {
let path = item.rawValue.origin.path
let url = contentsURL.appendingPathComponent(path.value)
let assetsURL = url.appending(path: assetsPath)
let filteredAssets = filterFilePaths(
from: content.rawValue.assets,
input: property.input
)
guard !filteredAssets.isEmpty else {
continue
}
let assetKeys =
filteredAssets.compactMap {
$0.split(separator: ".").first
}
.map(String.init)
let resolvedAssets = filteredAssets.map {
"./\(assetsPath)/\($0)"
.resolveAsset(
baseURL: baseURL,
assetsPath: assetsPath,
slug: content.slug.value
)
}
let frontMatter = item.rawValue.markdown.frontMatter
let finalAssets =
property.resolvePath ? resolvedAssets : filteredAssets
switch property.action {
case .add:
if let originalItems = frontMatter[property.property]?
.arrayValue(as: String.self)
{
item.properties[property.property] = .init(
originalItems + finalAssets
)
}
else {
item.properties[property.property] = .init(finalAssets)
}
case .set:
if finalAssets.count == 1 {
let asset = finalAssets[0]
item.properties[property.property] = .init(asset)
}
else {
item.properties[property.property] = .init(
createDictionaryValues(
assetKeys: assetKeys,
array: finalAssets
)
)
}
case .load:
if filteredAssets.count == 1 {
let asset = filteredAssets[0]
let url = assetsURL.appending(path: asset)
let contents = try String(
contentsOf: url,
encoding: .utf8
)
item.properties[property.property] = .init(contents)
}
else {
var values: [String: AnyCodable] = [:]
for i in 0..<filteredAssets.count {
let asset = filteredAssets[i]
let url = assetsURL.appending(path: asset)
let contents = try String(
contentsOf: url,
encoding: .utf8
)
values[assetKeys[i]] = .init(contents)
}
item.properties[property.property] = .init(values)
}
// TODO: check extension, add json support
case .parse:
if filteredAssets.count == 1 {
let asset = filteredAssets[0]
let url = assetsURL.appending(path: asset)
let data = try Data(contentsOf: url)
let yaml = try ToucanYAMLDecoder()
.decode(AnyCodable.self, from: data)
item.properties[property.property] = yaml
}
else {
var values: [String: AnyCodable] = [:]
for i in 0..<filteredAssets.count {
let asset = filteredAssets[i]
let url = assetsURL.appending(path: asset)
let data = try Data(contentsOf: url)
let yaml = try ToucanYAMLDecoder()
.decode(AnyCodable.self, from: data)
values[assetKeys[i]] = yaml
}
item.properties[property.property] = .init(values)
}
}
}
results.append(item)
}
return results
}
func applyBehaviors(
pipeline: Pipeline,
to contents: [Content],
contentsURL: URL,
assetsPath: String
) throws -> [PipelineResult] {
var results: [PipelineResult] = []
for content in contents {
var assetsReady: Set<String> = .init()
for behavior in pipeline.assets.behaviors {
let isAllowed = pipeline.contentTypes.isAllowed(
contentType: content.type.id
)
guard isAllowed else {
continue
}
let remainingAssets = Set(content.rawValue.assets)
.subtracting(assetsReady)
let matchingRemainingAssets = filterFilePaths(
from: Array(remainingAssets),
input: behavior.input
)
guard !matchingRemainingAssets.isEmpty else {
continue
}
for inputAsset in matchingRemainingAssets {
let basePath = content.rawValue.origin.path
let sourcePath = [
basePath.value,
assetsPath,
inputAsset,
]
.joined(separator: "/")
let file = getNameAndExtension(from: inputAsset)
let destPath = [
assetsPath,
content.slug.value,
inputAsset,
]
.filter { !$0.isEmpty }
.joined(separator: "/")
.split(separator: "/")
.dropLast()
.joined(separator: "/")
logger.trace(
"Resolving matching asset behavior.",
metadata: [
"behavior": .string(behavior.id),
"source": .string(sourcePath),
"destination": .string(destPath),
]
)
let fileURL = contentsURL.appending(path: sourcePath)
switch behavior.id {
case CompileSASSBehavior.id:
let script = try CompileSASSBehavior()
let css = try script.run(fileURL: fileURL)
// TODO: proper output management later on
results.append(
.init(
source: .asset(css),
destination: .init(
path: destPath,
file: behavior.output.name,
ext: behavior.output.ext
)
)
)
case MinifyCSSBehavior.id:
let script = MinifyCSSBehavior()
let css = try script.run(fileURL: fileURL)
results.append(
.init(
source: .asset(css),
destination: .init(
path: destPath,
file: behavior.output.name,
ext: behavior.output.ext
)
)
)
default: // copy
results.append(
.init(
source: .assetFile(sourcePath),
destination: .init(
path: destPath,
file: file.name,
ext: file.ext
)
)
)
}
assetsReady.insert(inputAsset)
}
}
}
return results
}
}
================================================
FILE: Sources/ToucanSDK/Content/ContentTypeResolver.swift
================================================
//
// ContentTypeResolver.swift
// Toucan
//
// Created by Tibor Bödecs on 2025. 05. 30..
//
import ToucanCore
import ToucanSource
enum ContentTypeResolverError: ToucanError {
case missingContentType(String, String)
case unknown(Error)
var underlyingErrors: [any Error] {
switch self {
case .missingContentType:
[]
case let .unknown(error):
[error]
}
}
var logMessage: String {
switch self {
case let .missingContentType(id, path):
"Missing content type for identifier: `\(id)` at `\(path)`."
case let .unknown(error):
error.localizedDescription
}
}
var userFriendlyMessage: String {
switch self {
case .missingContentType:
"Missing content type."
case .unknown:
"Unknown content conversion error."
}
}
}
struct ContentTypeResolver {
let contentTypes: [ContentType]
init(
types: [ContentType],
pipelines: [Pipeline]
) {
let virtualTypes = pipelines.compactMap {
$0.definesType ? ContentType(id: $0.id) : nil
}
self.contentTypes = (types + virtualTypes).sorted { $0.id < $1.id }
}
func getContentType(
for origin: Origin,
using id: String?
) throws(ContentTypeResolverError) -> ContentType {
if let id {
guard
let result = contentTypes.first(where: { $0.id == id })
else {
throw .missingContentType(id, origin.path.value)
}
return result
}
if let type = contentTypes.first(
where: { type in
type.paths.contains { origin.path.value.hasPrefix($0) }
}
) {
return type
}
let results = contentTypes.filter(\.default)
precondition(
!results.isEmpty,
"Don't forget to validate build target first."
)
return results[0]
}
}
================================================
FILE: Sources/ToucanSDK/Content/IteratorInfo.swift
================================================
//
// IteratorInfo.swift
// Toucan
//
// Created by gerp83 on 2025. 04. 17..
//
/// Provides pagination and iteration metadata for a content collection,
/// used when rendering paginated list views.
public struct IteratorInfo {
/// Represents a navigation link within a paginated content sequence.
public struct Link: Codable {
/// The page number this link points to.
public var number: Int
/// The permalink URL for this page.
public var permalink: String
/// Whether this link refers to the currently active page.
public var isCurrent: Bool
/// Initializes a new pagination link.
///
/// - Parameters:
/// - number: The page number.
/// - permalink: The URL for that page.
/// - isCurrent: Whether this link is for the current page.
public init(
number: Int,
permalink: String,
isCurrent: Bool
) {
self.number = number
self.permalink = permalink
self.isCurrent = isCurrent
}
}
/// The current page number (1-based).
public var current: Int
/// The total number of pages in the iterator.
public var total: Int
/// The number of items per page.
public var limit: Int
/// The subset of `Content` items that belong to the current page.
public var items: [Content]
/// A list of links to all available pages for UI navigation.
public var links: [Link]
/// An optional scope key used to identify the context or view this iterator belongs to.
///
/// This can help differentiate between multiple iterators for the same content type
/// (e.g., "allPosts", "featuredPosts").
public var scope: String?
/// Initializes a new iterator metadata structure.
///
/// - Parameters:
/// - current: The current page number.
/// - total: The total number of pages.
/// - limit: Items per page.
/// - items: The content items for the current page.
/// - links: Pagination links to all pages.
/// - scope: An optional scope identifier.
public init(
current: Int,
total: Int,
limit: Int,
items: [Content],
links: [Link],
scope: String?
) {
self.current = current
self.total = total
self.limit = limit
self.items = items
self.links = links
self.scope = scope
}
}
================================================
FILE: Sources/ToucanSDK/Content/Query+Resolve.swift
================================================
//
// Query+Resolve.swift
// Toucan
//
// Created by Tibor Bödecs on 2025. 05. 16..
//
import ToucanSource
public extension Condition {
/// Recursively resolves dynamic placeholders in the condition using a parameter map.
///
/// Placeholders must be strings in the form `{{parameterKey}}` and will be
/// replaced by values from the given parameters dictionary.
///
/// - Parameter parameters: A dictionary of key-value pairs to substitute into the condition.
/// - Returns: A new `Condition` with resolved values where applicable.
func resolve(with parameters: [String: AnyCodable]) -> Self {
switch self {
case let .field(key, op, value):
guard
let stringValue = value.value(as: String.self),
stringValue.count > 4,
stringValue.hasPrefix("{{"),
stringValue.hasSuffix("}}")
else {
return self
}
let paramKeyToUse = String(stringValue.dropFirst(2).dropLast(2))
guard let newValue = parameters[paramKeyToUse] else {
return self
}
return .field(key: key, operator: op, value: newValue)
case let .and(conditions):
return .and(conditions.map { $0.resolve(with: parameters) })
case let .or(conditions):
return .or(conditions.map { $0.resolve(with: parameters) })
}
}
}
public extension Query {
/// Resolves dynamic filter parameters by injecting values into the filter condition tree.
///
/// This is useful when filters include placeholders that need to be resolved at runtime.
///
/// - Parameter parameters: A dictionary of key-value pairs to replace placeholders in the filter.
/// - Returns: A new `Query` instance with resolved filter conditions.
func resolveFilterParameters(
with parameters: [String: AnyCodable]
) -> Self {
.init(
contentType: contentType,
scope: scope,
limit: limit,
offset: offset,
filter: filter?.resolve(with: parameters),
orderBy: orderBy
)
}
}
================================================
FILE: Sources/ToucanSDK/Content/RelationValue.swift
================================================
//
// RelationValue.swift
// Toucan
//
// Created by Tibor Bödecs on 2025. 01. 30..
//
import ToucanSource
/// Represents the resolved value of a relation in content, including the target content type,
/// the relation's cardinality, and the identifiers of related items.
public struct RelationValue {
/// The type of content this relation points to (e.g., `"author"`, `"post"`, `"product"`).
public var contentType: String
/// The relation type indicating if it's a one-to-one or one-to-many relationship.
public var type: RelationType
/// A list of string identifiers for the related content items.
/// For `.one`, this should typically contain a single ID; for `.many`, multiple.
public var identifiers: [String]
/// Initializes a new `RelationValue` representing the resolved target(s) of a content relation.
///
/// - Parameters:
/// - contentType: The name of the target content type.
/// - type: The type of relation (single or multiple).
/// - identifiers: A list of string IDs pointing to related content.
public init(
contentType: String,
type: RelationType,
identifiers: [String]
) {
self.contentType = contentType
self.type = type
self.identifiers = identifiers
}
}
================================================
FILE: Sources/ToucanSDK/DateFormats/DateContext.swift
================================================
//
// DateContext.swift
// Toucan
//
// Created by Tibor Bödecs on 2025. 02. 12..
//
/// A configuration container for date, time, and custom date-time formatting patterns.
///
/// `DateFormats` includes predefined formatting levels (full, long, medium, short)
/// for both dates and times, as well as support for arbitrary format labels.
public struct DateContext: Codable {
/// Represents standardized formatting levels for a date or time value.
///
/// These levels mirror common locale-aware date style options.
public struct Standard: Codable {
/// A fully verbose date format (e.g., `"EEEE, MMMM d, yyyy"`).
public var full: String
/// A long-form date format (e.g., `"MMMM d, yyyy"`).
public var long: String
/// A medium-form date format (e.g., `"MMM d, yyyy"`).
public var medium: String
/// A short-form date format (e.g., `"M/d/yy"`).
public var short: String
/// Initializes a new `Standard` date format set.
///
/// - Parameters:
/// - full: Full verbose date format string.
/// - long: Long format string.
/// - medium: Medium format string.
/// - short: Short format string.
public init(
full: String,
long: String,
medium: String,
short: String
) {
self.full = full
self.long = long
self.medium = medium
self.short = short
}
}
/// Standardized date format strings (e.g., full, medium, short).
public var date: Standard
/// Standardized time format strings (e.g., full, medium, short).
public var time: Standard
/// A standard iso8601 date string.
public var iso8601: String
/// A Unix timestamp representing a default or reference point in time.
public var timestamp: Double
/// Additional named date format strings keyed by label.
///
/// These can be used for custom formatting beyond the standard levels.
public var formats: [String: String]
/// Initializes a `DateFormats` configuration.
///
/// - Parameters:
/// - date: Standardized date formatting options.
/// - time: Standardized time formatting options.
/// - timestamp: A base or reference timestamp, typically in Unix format.
/// - iso8601: A standard iso8601 date string.
/// - formats: Custom named format strings for specialized use cases.
public init(
date: Standard,
time: Standard,
timestamp: Double,
iso8601: String,
formats: [String: String]
) {
self.date = date
self.time = time
self.timestamp = timestamp
self.iso8601 = iso8601
self.formats = formats
}
}
================================================
FILE: Sources/ToucanSDK/DateFormats/ToucanDateFormatters.swift
================================================
//
// ToucanDateFormatters.swift
// Toucan
//
// Created by Tibor Bödecs on 2025. 05. 26..
//
import Foundation
import Logging
import ToucanCore
import ToucanSource
/// ```
/// target:
/// dev:
/// input: ./src
/// output: ./dist
/// config: ./src/config.dev.yml => auto lookup like this?
/// -> default looks up for config.yml
///
/// live:
/// config: ./src/config.live.yml
///
/// config.dev.yml:
/// url: http://localhost:3000/
///
/// # output date formats basis
///
/// date:
/// input:
/// # input date formats basis
/// locale: en-US
/// timezone: Americas/Los_Angeles
/// format: yyyy-MM-dd'T'HH:mm:ss.SSS'Z'
/// output:
/// locale: en-US
/// timezone: Americas/Los_Angeles
/// formats:
/// year:
/// format: "y"
/// locale: hu-HU
/// timezone: Europe/Budapest
///
/// pipeline -> overrides config completely
/// date:
/// input:
/// locale: ???
/// timezone: ???
/// format: yyyy-MM-dd'T'HH:mm:ss.SSS'Z'
/// output:
/// locale: en-US
/// timezone: Americas/Los_Angeles
/// formats:
/// year:
/// format: "y"
/// locale: ???
/// timezone: ???
///
/// # content type
/// post
/// publication:
/// type: date
/// config: # input
/// format:
/// locale:
/// timeZone:
/// ```
/// Extension to configure `DateFormatter` with localization and config options.
private extension DateFormatter {
/// Creates and configures a `DateFormatter`.
///
/// - Parameters:
/// - localization: The locale and time zone settings to apply.
/// - block: A closure to further configure the formatter.
/// - Returns: A fully configured `DateFormatter`.
static func build(
using localization: DateLocalization = .defaults,
_ block: (inout DateFormatter) -> Void
) -> DateFormatter {
var formatter = DateFormatter()
formatter.use(localization: localization)
formatter.dateStyle = .none
formatter.timeStyle = .none
block(&formatter)
return formatter
}
/// Applies the given localization (locale and time zone) to the formatter.
///
/// - Parameter localization: The locale and time zone options.
func use(localization: DateLocalization) {
let id = Locale.identifier(.icu, from: localization.locale)
locale = .init(identifier: id)
timeZone = .init(identifier: localization.timeZone)
}
/// Applies a `DateFormatterConfig` (format, locale, time zone) to the formatter.
///
/// - Parameter config: The date formatter configuration.
func use(config: DateFormatterConfig) {
use(localization: config.localization)
dateFormat = config.format
}
}
/// Holds system date and time style `DateFormatter` instances and an ISO8601 formatter.
private struct SystemDateFormatters {
struct Date {
var full: DateFormatter
var long: DateFormatter
var medium: DateFormatter
var short: DateFormatter
}
struct Time {
var full: DateFormatter
var long: DateFormatter
var medium: DateFormatter
var short: DateFormatter
}
var date: Date
var time: Time
var iso8601: DateFormatter
}
/// Main utility for parsing and formatting dates based on project configuration.
///
/// Combines input parsing, system-style formatters, and user-defined formats.
public struct ToucanInputDateFormatter {
private var dateConfig: Config.DataTypes.Date
private var inputFormatter: DateFormatter
private var ephemeralFormatter: DateFormatter
var logger: Logger
/// Initializes the date formatter utility.
///
/// - Parameters:
/// - dateConfig: The base date configuration from the project.
/// - logger: A logger instance for diagnostics.
public init(
dateConfig: Config.DataTypes.Date,
logger: Logger = .subsystem("input-date-formatter")
) {
self.dateConfig = dateConfig
self.inputFormatter = .build { $0.use(config: dateConfig.input) }
self.ephemeralFormatter = .build { $0.use(config: dateConfig.input) }
self.logger = logger
}
/// Parses a date string into a `Date` object.
///
/// - Parameters:
/// - string: The string representation of the date.
/// - config: Optional `DateFormatterConfig` to override the input format.
/// - Returns: A `Date` if parsing succeeds, otherwise `nil`.
public func date(
from string: String,
using config: DateFormatterConfig? = nil
) -> Date? {
if let config {
ephemeralFormatter.use(config: config)
return ephemeralFormatter.date(from: string)
}
return inputFormatter.date(from: string)
}
/// Converts a date into a `String` object.
///
/// - Parameters:
/// - date: The date representation.
/// - config: Optional `DateFormatterConfig` to override the input format.
/// - Returns: A `String` using the provided date format config.
public func string(
from date: Date,
using config: DateFormatterConfig? = nil
) -> String {
if let config {
ephemeralFormatter.use(config: config)
return ephemeralFormatter.string(from: date)
}
return inputFormatter.string(from: date)
}
}
/// Main utility for parsing and formatting dates based on project configuration.
///
/// Combines input parsing, system-style formatters, and user-defined formats.
public struct ToucanOutputDateFormatter {
private var dateConfig: Config.DataTypes.Date
private var pipelineDateConfig: Pipeline.DataTypes.Date?
private var systemFormatters: SystemDateFormatters
private var userFormatters: [String: DateFormatter]
var logger: Logger
/// Initializes the date formatter utility.
///
/// - Parameters:
/// - dateConfig: The base date configuration from the project.
/// - pipelineDateConfig: Optional overrides for date configuration.
/// - logger: A logger instance for diagnostics.
public init(
dateConfig: Config.DataTypes.Date,
pipelineDateConfig: Pipeline.DataTypes.Date? = nil,
logger: Logger = .subsystem("date-formatter")
) {
self.dateConfig = dateConfig
self.pipelineDateConfig = pipelineDateConfig
var localization = dateConfig.output
if let outputLocalization = pipelineDateConfig?.output {
localization = outputLocalization
}
self.systemFormatters = .init(
date: .init(
full: .build(using: localization) { $0.dateStyle = .full },
long: .build(using: localization) { $0.dateStyle = .long },
medium: .build(using: localization) { $0.dateStyle = .medium },
short: .build(using: localization) { $0.dateStyle = .short }
),
time: .init(
full: .build(using: localization) { $0.timeStyle = .full },
long: .build(using: localization) { $0.timeStyle = .long },
medium: .build(using: localization) { $0.timeStyle = .medium },
short: .build(using: localization) { $0.timeStyle = .short }
),
iso8601: .build(using: localization) {
$0.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
}
)
self.userFormatters = [:]
self.logger = logger
let userFormatterConfig = dateConfig.formats.merging(
(pipelineDateConfig?.formats ?? [:]),
uniquingKeysWith: { _, new in new }
)
for (key, config) in userFormatterConfig {
userFormatters[key] = .build(using: localization) {
$0.use(config: config)
}
}
}
/// Formats a `Date` into a `DateContext`, providing multiple style outputs and custom formats.
///
/// - Parameter date: The `Date` to format.
/// - Returns: A `DateContext` containing formatted strings and timestamp.
public func format(
_ date: Date
) -> DateContext {
.init(
date: .init(
full: systemFormatters.date.full.string(from: date),
long: systemFormatters.date.long.string(from: date),
medium: systemFormatters.date.medium.string(from: date),
short: systemFormatters.date.short.string(from: date)
),
time: .init(
full: systemFormatters.time.full.string(from: date),
long: systemFormatters.time.long.string(from: date),
medium: systemFormatters.time.medium.string(from: date),
short: systemFormatters.time.short.string(from: date)
),
timestamp: date.timeIntervalSince1970,
iso8601: systemFormatters.iso8601.string(from: date),
formats: userFormatters.mapValues { $0.string(from: date) }
)
}
/// Formats a time interval since 1970 into a `DateContext`.
///
/// - Parameter timestamp: The time interval (seconds since 1970).
/// - Returns: A `DateContext` with formatted outputs.
public func format(
_ timestamp: TimeInterval
) -> DateContext {
format(.init(timeIntervalSince1970: timestamp))
}
}
================================================
FILE: Sources/ToucanSDK/Models/ContextBundle.swift
================================================
//
// ContextBundle.swift
// Toucan
//
// Created by Tibor Bödecs on 2025. 02. 21..
//
import ToucanSource
/// A bundle containing a single content item, its rendering context, and its destination metadata.
///
/// `ContextBundle` is typically used as an input for template rendering or output generation,
/// combining the actual content with any supplemental data required for processing.
public struct ContextBundle {
/// The primary content item to be rendered or processed.
public var content: Content
/// A key-value store representing the extended rendering context (e.g., metadata, global variables).
/// These values can be used during template evaluation or logic processing.
public var context: [String: AnyCodable]
/// The intended destination of the output generated from this bundle.
public var destination: Destination
/// Initializes a new `ContextBundle` with content, context data, and a destination.
///
/// - Parameters:
/// - content: The `Content` instance to render.
/// - context: A context dictionary providing additional rendering metadata or variables.
/// - destination: Where the rendered output should be saved.
public init(
content: Content,
context: [String: AnyCodable],
destination: Destination
) {
self.content = content
self.context = context
self.destination = destination
}
}
================================================
FILE: Sources/ToucanSDK/Models/Destination.swift
================================================
//
// Destination.swift
// Toucan
//
// Created by Tibor Bödecs on 2025. 02. 21..
//
/// Represents the destination location and filename for rendered or transformed content.
public struct Destination: Sendable {
/// The relative or absolute path to the target directory where the file should be placed.
public var path: String
/// The base name of the file (without extension).
public var file: String
/// The file extension (e.g., "html", "json", "md").
public var ext: String
/// Initializes a new `Destination` describing where and how a file should be written.
///
/// - Parameters:
/// - path: The directory path to write the file to.
/// - file: The base file name (without extension).
/// - ext: The file extension (e.g., `"html"`, `"json"`).
public init(
path: String,
file: String,
ext: String
) {
self.path = path
self.file = file
self.ext = ext
}
}
================================================
FILE: Sources/ToucanSDK/Models/PipelineResult.swift
================================================
//
// PipelineResult.swift
// Toucan
//
// Created by Tibor Bödecs on 2025. 02. 21..
//
/// Represents the output of a content transformation pipeline, including the
/// transformed content and its intended destination.
public struct PipelineResult: Sendable {
/// The source material for the pipeline result.
public enum Source: Sendable {
/// An asset source, that needs top be copied.
case assetFile(String)
/// A generated asset source
case asset(String)
/// The final transformed content (e.g., HTML, Markdown, etc.).
case content(String)
/// A Boolean value indicating whether the pipeline result's source is content-based.
/// Returns `true` if the source is `.content`, otherwise `false`.
public var isContent: Bool {
!isAsset
}
/// A Boolean value indicating whether the pipeline result's source is an asset.
/// Returns `true` if the source is `.asset`, otherwise `false`.
public var isAsset: Bool {
switch self {
case .content:
false
case .assetFile:
true
case .asset:
true
}
}
}
/// The source material.
public var source: Source
/// The destination metadata describing where or how the content should be output.
public var destination: Destination
/// Initializes a new `PipelineResult` with transformed content and a destination.
///
/// - Parameters:
/// - source: The source material.
/// - destination: A `Destination` indicating where the result should be saved or rendered.
public init(
source: Source,
destination: Destination
) {
self.source = source
self.destination = destination
}
}
================================================
FILE: Sources/ToucanSDK/Models/Slug.swift
================================================
//
// Slug.swift
// Toucan
//
// Created by gerp83 on 2025. 04. 17..
//
/// A value type representing a URL-friendly identifier for a content item.
public struct Slug: Equatable {
/// The raw slug string (e.g., `"blog/welcome"`, `"about"`, `""`).
public var value: String
/// Initializes a new slug.
///
/// - Parameter value: The raw slug string.
public init(
_ value: String
) {
self.value = value
}
/// Extracts a dynamic iterator identifier from a slug value containing
/// a templated range (e.g., `"blog/{{page}}"` → `"page"`).
///
/// - Returns: The identifier inside `{{...}}`, or `nil` if not found.
public func extractIteratorID() -> String? {
guard
let startRange = value.range(of: "{{"),
let endRange = value.range(
of: "}}",
range: startRange.upperBound..<value.endIndex
)
else {
return nil
}
return .init(value[startRange.upperBound..<endRange.lowerBound])
}
/// Constructs a permalink from the base URL and the slug.
///
/// - Parameter baseURL: The base URL of the site (e.g., `"https://example.com"`).
/// - Returns: A fully-qualified permalink string (e.g., `"https://example.com/blog/"`).
public func permalink(baseURL: String) -> String {
let components = value.split(separator: "/").map(String.init)
if components.isEmpty {
return baseURL.ensureTrailingSlash()
}
if components.last?.split(separator: ".").count ?? 0 > 1 {
// If last segment has a file extension, return without trailing slash
return ([baseURL] + components).joined(separator: "/")
}
return ([baseURL] + components)
.joined(separator: "/")
.ensureTrailingSlash()
}
}
extension Slug: Codable {
/// Generates a context-aware identifier string based on the last path component of a value.
///
public func contextAwareIdentifier() -> String {
.init(value.split(separator: "/").last ?? "")
}
/// Creates a new instance by decoding from the given decoder.
///
/// This initializer attempts to decode the value as a single string.
///
/// - Parameter decoder: The decoder to read data from.
/// - Throws: An error if reading from the decoder fails, or if the data is not a single string.
public init(
from decoder: Decoder
) throws {
let container = try decoder.singleValueContainer()
self.value = try container.decode(String.self)
}
/// Encodes this value into the given encoder.
///
/// This method encodes the value as a single string.
///
/// - Parameter encoder: The encoder to write data to.
/// - Throws: An error if encoding fails.
public func encode(
to encoder: Encoder
) throws {
var container = encoder.singleValueContainer()
try container.encode(value)
}
}
================================================
FILE: Sources/ToucanSDK/Outputs/ContextBundleToHTMLRenderer.swift
================================================
//
// ContextBundleToHTMLRenderer.swift
// Toucan
//
// Created by Viasz-Kádi Ferenc on 2025. 05. 13..
//
import Foundation
import Logging
import ToucanSource
struct ContextBundleToHTMLRenderer {
let mustacheRenderer: MustacheRenderer
let engineContentTypesOptions: [String: AnyCodable]
let pipelineViewKey: String
let logger: Logger
init(
pipeline: Pipeline,
templates: [String: String],
logger: Logger = .subsystem("context-bundle-to-html-renderer")
) throws {
self.mustacheRenderer = try MustacheRenderer(
templates: templates.mapValues {
try .init(string: $0)
}
)
let engineOptions = pipeline.engine.options
self.engineContentTypesOptions = engineOptions.dict("contentTypes")
self.pipelineViewKey = [
ViewFrontMatterKeys.views.rawValue, pipeline.id,
]
.joined(separator: ".")
self.logger = logger
}
func render(
_ contextBundles: [ContextBundle]
) -> [PipelineResult] {
contextBundles.compactMap { render($0) }
}
func render(
_ contextBundle: ContextBundle
) -> PipelineResult? {
let contentTypeOptions = engineContentTypesOptions.dict(
contextBundle.content.type.id
)
let frontMatter = contextBundle.content.rawValue.markdown.frontMatter
let contentTypeView = contentTypeOptions.string(
ViewFrontMatterKeys.view.rawValue
)
let genericContentView = frontMatter.string(
ViewFrontMatterKeys.any.rawValue
)
let contentView = frontMatter.string(pipelineViewKey)
let viewId = contentView ?? genericContentView ?? contentTypeView
guard let viewId, !viewId.isEmpty else {
logger.warning(
"No view has been specified for this content.",
metadata: [
"slug": "\(contextBundle.content.slug.value)",
"type": "\(contextBundle.content.type.id)",
]
)
return nil
}
let html = mustacheRenderer.render(
using: viewId,
with: contextBundle.context
)
guard let html else {
logger.warning(
"Could not get valid HTML from content using view.",
metadata: [
"slug": .string(contextBundle.content.slug.value),
"type": .string(contextBundle.content.type.id),
"view": .string(viewId),
]
)
return nil
}
return .init(
source: .content(html),
destination: contextBundle.destination
)
}
}
================================================
FILE: Sources/ToucanSDK/Outputs/ContextBundleToJSONRenderer.swift
================================================
//
// ContextBundleToJSONRenderer.swift
// Toucan
//
// Created by Viasz-Kádi Ferenc on 2025. 05. 13..
//
import Foundation
import Logging
import ToucanSource
struct ContextBundleToJSONRenderer {
let pipeline: Pipeline
let encoder: JSONEncoder
let logger: Logger
let keyPath: String?
let keyPaths: [String: AnyCodable]?
init(
pipeline: Pipeline,
logger: Logger = .subsystem("context-bundle-to-json-renderer")
) {
let encoder = JSONEncoder()
encoder.outputFormatting = [
.prettyPrinted,
.withoutEscapingSlashes,
.sortedKeys,
]
self.pipeline = pipeline
self.encoder = encoder
self.logger = logger
self.keyPath = pipeline.engine.options.string("keyPath")
self.keyPaths = pipeline.engine.options.value(
"keyPaths",
as: [String: AnyCodable].self
)
}
private func data(
from context: [String: Any],
at keyPath: String?,
using encoder: JSONEncoder
) throws -> Data? {
guard let keyPath else {
return nil
}
if let value = context.value(forKeyPath: keyPath) {
return try encoder.encode(AnyCodable(value))
}
return nil
}
private func data(
from context: [String: Any],
keyPaths: [String: AnyCodable]?,
using encoder: JSONEncoder
) throws -> Data? {
var result: [String: AnyCodable] = [:]
guard let keyPaths else {
return nil
}
for (keyPath, value) in keyPaths {
guard let newKeyPath = value.value(as: String.self) else {
continue
}
if let value = context.value(forKeyPath: keyPath) {
result[newKeyPath] = .init(value)
}
}
return try encoder.encode(result)
}
func render(_ contextBundles: [ContextBundle]) -> [PipelineResult] {
contextBundles.compactMap {
render($0)
}
}
func render(_ contextBundle: ContextBundle) -> PipelineResult? {
let metadata: Logger.Metadata = [
"slug": "\(contextBundle.content.slug.value)"
]
let context = contextBundle.context
let unboxedContext = context.unboxed(encoder)
let encodedData = firstSucceeding([
{
try data(
from: unboxedContext,
keyPaths: keyPaths,
using: encoder
)
},
{ try data(from: unboxedContext, at: keyPath, using: encoder) },
{ try encoder.encode(context) },
])
guard let encodedData else {
logger.warning(
"Could not encode context data as JSON object.",
metadata: metadata
)
return nil
}
let json = String(data: encodedData, encoding: .utf8)
guard let json else {
logger.warning(
"Could not encode context data as JSON output.",
metadata: metadata
)
return nil
}
return .init(
source: .content(json),
destination: contextBundle.destination
)
}
}
================================================
FILE: Sources/ToucanSDK/Renderers/BuildTargetSourceRenderer.swift
================================================
//
// BuildTargetSourceRenderer.swift
// Toucan
//
// Created by Viasz-Kádi Ferenc on 2025. 03. 25..
//
import Foundation
import Logging
import ToucanCore
import ToucanMarkdown
import ToucanSerialization
import ToucanSource
enum BuildTargetSourceRendererError: ToucanError {
case invalidEngine(String)
case unknown(Error)
var underlyingErrors: [any Error] {
switch self {
case let .unknown(error):
[error]
default:
[]
}
}
var logMessage: String {
switch self {
case let .invalidEngine(engine):
"Invalid engine: `\(engine)`."
case let .unknown(error):
error.localizedDescription
}
}
var userFriendlyMessage: String {
switch self {
case let .invalidEngine(engine):
"Invalid engine: `\(engine)`."
case .unknown:
"Unknown source validator error."
}
}
}
/// Responsible for rendering the entire site bundle based on the `BuildTargetSource` configuration.
///
/// It processes content pipelines using the configured engine (Mustache, JSON, etc.),
/// resolves content and site-level context, and outputs rendered content using templates
/// or encoded formats.
public struct BuildTargetSourceRenderer {
/// Site configuration + all raw content
let buildTargetSource: BuildTargetSource
/// Generator metadata (e.g., version, name)
let generatorInfo: GeneratorInfo
/// Logger for warnings and errors
let logger: Logger
/// Cache
var contentContextCache: [String: [String: AnyCodable]] = [:]
/// Initializes a renderer from a source bundle.
///
/// - Parameters:
/// - buildTargetSource: The structured bundle containing settings, pipelines, and contents.
/// - generatorInfo: Info about the content generator (defaults to `.current`).
/// - logger: Logger for reporting issues or metrics.
public init(
buildTargetSource: BuildTargetSource,
generatorInfo: GeneratorInfo = .current,
logger: Logger = .subsystem("build-target-source-renderer")
) {
self.buildTargetSource = buildTargetSource
self.generatorInfo = generatorInfo
self.logger = logger
}
/// Returns the last content update based on the pipeline config
private func getLastContentUpdate(
contents: [Content],
pipeline: Pipeline,
now: TimeInterval
) -> TimeInterval? {
var updateTypes = Set(contents.map(\.type.id))
if !pipeline.contentTypes.lastUpdate.isEmpty {
updateTypes = updateTypes.filter {
pipeline.contentTypes.lastUpdate.contains($0)
}
}
let lastUpdate =
updateTypes.compactMap {
let items = contents.run(
query: .init(
contentType: $0,
scope: nil,
limit: 1,
orderBy: [
.init(
key: SystemPropertyKeys.lastUpdate.rawValue,
direction: .desc
)
]
),
now: now,
logger: logger
)
return items.first?.rawValue.lastModificationDate
}
.sorted(by: >).first
logger.trace(
"Last update for the pipeline.",
metadata: [
"pipeline": .string(pipeline.id),
"lastUpdate": .string(
Date(timeIntervalSince1970: lastUpdate ?? 0).description
),
]
)
return lastUpdate
}
private func baseURL() -> String {
buildTargetSource.target.url.dropTrailingSlash()
}
/// Returns the renderable context bundle for each content for a given pipeline using the global context
m
gitextract__gpz1oly/
├── .github/
│ └── workflows/
│ ├── actions.yml
│ ├── linux.yml
│ ├── macos.yml
│ ├── pr-for-formula.yml
│ └── tag_actions.yml
├── .gitignore
├── .swift-format
├── .swiftformat
├── .swiftformatignore
├── .swiftheaderignore
├── Docker/
│ ├── Dockerfile
│ └── Dockerfile.testing
├── LICENSE
├── Makefile
├── Package.resolved
├── Package.swift
├── README.md
├── Sources/
│ ├── ToucanCore/
│ │ ├── Extensions/
│ │ │ ├── Dictionary+Extensions.swift
│ │ │ ├── Logging+Extensions.swift
│ │ │ ├── String+Extensions.swift
│ │ │ └── URL+Extensions.swift
│ │ ├── GeneratorInfo.swift
│ │ ├── Logger.swift
│ │ └── ToucanError.swift
│ ├── ToucanMarkdown/
│ │ ├── Markdown/
│ │ │ ├── HTML.swift
│ │ │ ├── HTMLVisitor.swift
│ │ │ ├── MarkdownBlockDirective.swift
│ │ │ └── MarkdownToHTMLRenderer.swift
│ │ ├── MarkdownRenderer.swift
│ │ ├── Outline/
│ │ │ ├── Outline.swift
│ │ │ └── OutlineParser.swift
│ │ ├── ReadingTime/
│ │ │ └── ReadingTimeCalculator.swift
│ │ └── Transformers/
│ │ ├── ContentTransformer.swift
│ │ ├── TransformerExecutor.swift
│ │ └── TransformerPipeline.swift
│ ├── ToucanSDK/
│ │ ├── Behaviors/
│ │ │ ├── Behavior.swift
│ │ │ ├── CompileSASSBehavior.swift
│ │ │ └── MinifyCSSBehavior.swift
│ │ ├── Content/
│ │ │ ├── Content+Query.swift
│ │ │ ├── Content.swift
│ │ │ ├── ContentResolver.swift
│ │ │ ├── ContentTypeResolver.swift
│ │ │ ├── IteratorInfo.swift
│ │ │ ├── Query+Resolve.swift
│ │ │ └── RelationValue.swift
│ │ ├── DateFormats/
│ │ │ ├── DateContext.swift
│ │ │ └── ToucanDateFormatters.swift
│ │ ├── Models/
│ │ │ ├── ContextBundle.swift
│ │ │ ├── Destination.swift
│ │ │ ├── PipelineResult.swift
│ │ │ └── Slug.swift
│ │ ├── Outputs/
│ │ │ ├── ContextBundleToHTMLRenderer.swift
│ │ │ └── ContextBundleToJSONRenderer.swift
│ │ ├── Renderers/
│ │ │ ├── BuildTargetSourceRenderer.swift
│ │ │ └── MustacheRenderer.swift
│ │ ├── Toucan.swift
│ │ ├── Utilities/
│ │ │ ├── Any+AnyCodable.swift
│ │ │ ├── AnyCodable+Json.swift
│ │ │ ├── Array+AnyCodable.swift
│ │ │ ├── ContextKeys.swift
│ │ │ ├── CopyManager.swift
│ │ │ ├── Dictionary+AnyCodable.swift
│ │ │ ├── Encodable+Json.swift
│ │ │ └── FirstSucceeding.swift
│ │ └── Validators/
│ │ ├── BuildTargetSourceValidator.swift
│ │ └── TemplateValidator.swift
│ ├── ToucanSerialization/
│ │ ├── ToucanDecoder.swift
│ │ ├── ToucanDecoderError.swift
│ │ ├── ToucanEncoder.swift
│ │ ├── ToucanEncoderError.swift
│ │ ├── ToucanJSONDecoder.swift
│ │ ├── ToucanJSONEncoder.swift
│ │ ├── ToucanYAMLDecoder.swift
│ │ └── ToucanYAMLEncoder.swift
│ ├── ToucanSource/
│ │ ├── Errors/
│ │ │ ├── ObjectLoaderError.swift
│ │ │ ├── SourceLoaderError.swift
│ │ │ └── TemplateLoaderError.swift
│ │ ├── Extensions/
│ │ │ ├── Decoder+Validate.swift
│ │ │ ├── Dictionary+AnyCodable.swift
│ │ │ └── FileManagerKit+Extensions.swift
│ │ ├── Loaders/
│ │ │ ├── BuildTargetSourceLoader.swift
│ │ │ ├── ObjectLoader.swift
│ │ │ ├── RawContentLoader.swift
│ │ │ └── TemplateLoader.swift
│ │ ├── MarkdownParser.swift
│ │ ├── Models/
│ │ │ ├── BuildTargetSource.swift
│ │ │ ├── BuiltTargetSourceLocations.swift
│ │ │ ├── Markdown.swift
│ │ │ ├── Origin.swift
│ │ │ ├── Path.swift
│ │ │ ├── RawContent.swift
│ │ │ ├── Template.swift
│ │ │ └── View.swift
│ │ └── Objects/
│ │ ├── AnyCodable.swift
│ │ ├── Blocks/
│ │ │ ├── Block+Attribute.swift
│ │ │ ├── Block+Parameter.swift
│ │ │ └── Block.swift
│ │ ├── Config/
│ │ │ ├── Config+Blocks.swift
│ │ │ ├── Config+Contents.swift
│ │ │ ├── Config+DataTypes+Date.swift
│ │ │ ├── Config+DataTypes.swift
│ │ │ ├── Config+Location.swift
│ │ │ ├── Config+Pipelines.swift
│ │ │ ├── Config+Renderer+ParagraphStyles.swift
│ │ │ ├── Config+RendererConfig.swift
│ │ │ ├── Config+Site.swift
│ │ │ ├── Config+Templates.swift
│ │ │ ├── Config+Types.swift
│ │ │ └── Config.swift
│ │ ├── Date/
│ │ │ ├── DateFormatterConfig.swift
│ │ │ └── DateLocalization.swift
│ │ ├── Pipeline/
│ │ │ ├── Pipeline+Assets.swift
│ │ │ ├── Pipeline+ContentTypes.swift
│ │ │ ├── Pipeline+DataTypes+Date.swift
│ │ │ ├── Pipeline+DataTypes.swift
│ │ │ ├── Pipeline+Engine.swift
│ │ │ ├── Pipeline+Output.swift
│ │ │ ├── Pipeline+Scope+Context.swift
│ │ │ ├── Pipeline+Scope.swift
│ │ │ ├── Pipeline+Transformers+Transformer.swift
│ │ │ ├── Pipeline+Transformers.swift
│ │ │ └── Pipeline.swift
│ │ ├── Property/
│ │ │ ├── Property.swift
│ │ │ ├── PropertyType.swift
│ │ │ └── SystemPropertyKeys.swift
│ │ ├── Query/
│ │ │ ├── Condition.swift
│ │ │ ├── Direction.swift
│ │ │ ├── Operator.swift
│ │ │ ├── Order.swift
│ │ │ └── Query.swift
│ │ ├── Relation/
│ │ │ ├── Relation.swift
│ │ │ └── RelationType.swift
│ │ ├── Settings/
│ │ │ └── Settings.swift
│ │ ├── Target/
│ │ │ ├── Target.swift
│ │ │ └── TargetConfig.swift
│ │ └── Types/
│ │ └── ContentType.swift
│ ├── _GitCommitHash/
│ │ ├── git_commit_hash.c
│ │ └── include/
│ │ └── git_commit_hash.h
│ ├── toucan/
│ │ └── Entrypoint.swift
│ ├── toucan-generate/
│ │ └── Entrypoint.swift
│ ├── toucan-init/
│ │ ├── Download.swift
│ │ └── Entrypoint.swift
│ ├── toucan-serve/
│ │ ├── Entrypoint.swift
│ │ └── NotFoundMiddleware.swift
│ └── toucan-watch/
│ └── Entrypoint.swift
├── Tests/
│ ├── ToucanCoreTests/
│ │ ├── Extensions/
│ │ │ ├── StringExtensionsTestSuite.swift
│ │ │ └── URLExtensionsTestSuite.swift
│ │ └── ToucanCoreTestSuite.swift
│ ├── ToucanMarkdownTests/
│ │ ├── ContentRendererTestSuite.swift
│ │ ├── HTMLVisitorTestSuite.swift
│ │ ├── MarkdownBlockDirective+Mock.swift
│ │ ├── MarkdownBlockDirectiveTestSuite.swift
│ │ └── OutlineTestSuite.swift
│ ├── ToucanSDKTests/
│ │ ├── BuildTargetSource/
│ │ │ ├── BuildTargetSourceRendererTestSuite.swift
│ │ │ └── BuildTargetSourceValidatorTestSuite.swift
│ │ ├── Content/
│ │ │ ├── ContentQueryTestSuite.swift
│ │ │ └── ContentResolverTestSuite.swift
│ │ ├── DateFormatter/
│ │ │ └── ToucanDateFormatterTestSuite.swift
│ │ ├── E2ETestSuite.swift
│ │ ├── Files/
│ │ │ ├── MarkdownFile.swift
│ │ │ ├── MustacheFile.swift
│ │ │ ├── RawContentBundle.swift
│ │ │ └── YAMLFile.swift
│ │ ├── Mocks/
│ │ │ ├── Mocks+Blocks.swift
│ │ │ ├── Mocks+BuildTargetSources.swift
│ │ │ ├── Mocks+ContentTypes.swift
│ │ │ ├── Mocks+E2E.swift
│ │ │ ├── Mocks+Files.swift
│ │ │ ├── Mocks+Pipelines.swift
│ │ │ ├── Mocks+RawContents.swift
│ │ │ ├── Mocks+Templates.swift
│ │ │ ├── Mocks+Views.swift
│ │ │ └── Mocks.swift
│ │ ├── Template/
│ │ │ └── TemplateValidatorTestSuite.swift
│ │ ├── Toucan/
│ │ │ └── ToucanTestSuite.swift
│ │ └── Utilities/
│ │ ├── AnyCodableWrapTests.swift
│ │ ├── CopyManagerTestSuite.swift
│ │ ├── PrettyPrint.swift
│ │ ├── RecursiveMergeTests.swift
│ │ ├── SlugTests.swift
│ │ └── UnboxingTestSuite.swift
│ └── ToucanSourceTests/
│ ├── BuildTargetSourceLoaderTestSuite.swift
│ ├── Extensions/
│ │ └── FileManagerKitExtensionsTestSuite.swift
│ ├── Files/
│ │ └── YAMLFile.swift
│ ├── MarkdownParserTestSuite.swift
│ ├── Models/
│ │ └── BuildTargetSourceLocationsTestSuite.swift
│ ├── Objects/
│ │ ├── AnyCodableTestSuite.swift
│ │ ├── Config/
│ │ │ └── ConfigTestSuite.swift
│ │ ├── DateFormatting/
│ │ │ └── DateFormattingTestSuite.swift
│ │ ├── Pipeline/
│ │ │ ├── PipelineContentTypeTestSuite.swift
│ │ │ ├── PipelineScopeContextTestSuite.swift
│ │ │ ├── PipelineScopeTestSuite.swift
│ │ │ ├── PipelineTestSuite.swift
│ │ │ └── PipelineTransformersTestSuite.swift
│ │ ├── Property/
│ │ │ ├── PropertyTestSuite.swift
│ │ │ └── PropertyTypeTestSuite.swift
│ │ ├── Query/
│ │ │ ├── ConditionTestSuite.swift
│ │ │ ├── DirectionTestSuite.swift
│ │ │ ├── OperatorTestSuite.swift
│ │ │ ├── OrderTestSuite.swift
│ │ │ └── QueryTestSuite.swift
│ │ ├── Relation/
│ │ │ ├── RelationTestSuite.swift
│ │ │ └── RelationTypeTestSuite.swift
│ │ ├── Settings/
│ │ │ └── SettingsTestSuite.swift
│ │ ├── Target/
│ │ │ ├── TargetConfigTestSuite.swift
│ │ │ └── TargetTestSuite.swift
│ │ └── Types/
│ │ └── TypesTestSuite.swift
│ ├── RawContentLoaderTestSuite.swift
│ └── TemplateLoaderTestSuite.swift
└── scripts/
├── install-toucan.sh
├── packaging/
│ ├── deb.sh
│ ├── dmg.sh
│ ├── pkg.sh
│ ├── rpm.sh
│ └── toucan.spec
├── run-chmod.sh
└── uninstall-toucan.sh
Condensed preview — 217 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (989K chars).
[
{
"path": ".github/workflows/actions.yml",
"chars": 1377,
"preview": "name: Actions\n\non:\n pull_request:\n branches:\n - main\n\njobs:\n\n bb_checks:\n name: BB Checks\n uses: BinaryB"
},
{
"path": ".github/workflows/linux.yml",
"chars": 5622,
"preview": "name: Build, Test and Upload Linux Binaries for tag\non:\n workflow_call:\n inputs:\n version:\n required: tr"
},
{
"path": ".github/workflows/macos.yml",
"chars": 6525,
"preview": "name: Build and Publish macOS Binaries\non:\n workflow_call:\n inputs:\n version:\n required: true\n ty"
},
{
"path": ".github/workflows/pr-for-formula.yml",
"chars": 3023,
"preview": "name: Push Homebrew Formula\n\non:\n workflow_call:\n inputs:\n version:\n required: true\n type: string"
},
{
"path": ".github/workflows/tag_actions.yml",
"chars": 1585,
"preview": "name: Dispatch macOS and Linux Builds on new tag\n\non:\n push:\n tags:\n - 'v*'\n - '[0-9]*'\n\npermissions:\n co"
},
{
"path": ".gitignore",
"chars": 99,
"preview": ".DS_Store\n.swiftpm\n.build\n.vscode\n.obsidian\n**/dist\n**/docs\nTests/sites/benchmark/\nExamples/try-o/\n"
},
{
"path": ".swift-format",
"chars": 2124,
"preview": "{\n \"fileScopedDeclarationPrivacy\" : {\n \"accessLevel\" : \"private\"\n },\n \"indentation\" : {\n \"spaces\" : 4\n },\n \"m"
},
{
"path": ".swiftformat",
"chars": 995,
"preview": "--swiftversion 6\n\n--indent 4\n --indentstrings true\n --smarttabs true\n --xcodeindentation enabled\n\n--maxwidth 80"
},
{
"path": ".swiftformatignore",
"chars": 13,
"preview": "Package.swift"
},
{
"path": ".swiftheaderignore",
"chars": 110,
"preview": ".*\n*.c\n*.h\n*.txt\n*.html\n*.yaml\nREADME.md\nPackage.resolved\nMakefile\nLICENSE\nPackage.swift\nDocker/**\nscripts/**\n"
},
{
"path": "Docker/Dockerfile",
"chars": 1806,
"preview": "FROM swift:6.3-noble AS build\n\nWORKDIR /build\nCOPY ./Package.* ./\nRUN swift package resolve\nCOPY Sources ./Sources\nCOPY "
},
{
"path": "Docker/Dockerfile.testing",
"chars": 179,
"preview": "FROM swift:6.1\n\nWORKDIR /app\n\nCOPY . ./\n\nRUN swift package resolve\nRUN swift package clean\nRUN swift package update\n\nCMD"
},
{
"path": "LICENSE",
"chars": 1116,
"preview": "MIT License\n\nCopyright (c) 2018-2022 Tibor Bödecs\nCopyright (c) 2022-2025 Binary Birds Ltd.\n\nPermission is hereby grante"
},
{
"path": "Makefile",
"chars": 1841,
"preview": "SHELL=/bin/bash\n\n.PHONY: docker\n\nbaseUrl = https://raw.githubusercontent.com/BinaryBirds/github-workflows/refs/heads/mai"
},
{
"path": "Package.resolved",
"chars": 10584,
"preview": "{\n \"originHash\" : \"b0aa8e8208e365a7988670c760c7c1a87737dac7471c3bcff7b3381dbaabf37b\",\n \"pins\" : [\n {\n \"identit"
},
{
"path": "Package.swift",
"chars": 9002,
"preview": "// swift-tools-version: 6.1\nimport PackageDescription\n\nlet swiftSettings: [SwiftSetting] = [\n .enableExperimentalFeat"
},
{
"path": "README.md",
"chars": 980,
"preview": "# Toucan\n\nToucan is a markdown-based Static Site Generator (SSG) written in Swift.\n\n## Installation\n\n## Compile from sou"
},
{
"path": "Sources/ToucanCore/Extensions/Dictionary+Extensions.swift",
"chars": 2704,
"preview": "//\n// Dictionary+Extensions.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 02. 03..\n//\n\npublic extension Dict"
},
{
"path": "Sources/ToucanCore/Extensions/Logging+Extensions.swift",
"chars": 3211,
"preview": "//\n// Logging+Extensions.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 18..\n//\n\nimport class Foundation."
},
{
"path": "Sources/ToucanCore/Extensions/String+Extensions.swift",
"chars": 6761,
"preview": "//\n// String+Extensions.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 17..\n//\n\nimport Foundation\n\npublic"
},
{
"path": "Sources/ToucanCore/Extensions/URL+Extensions.swift",
"chars": 1082,
"preview": "//\n// URL+Extensions.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 17..\n//\n\nimport Foundation\n\npublic ex"
},
{
"path": "Sources/ToucanCore/GeneratorInfo.swift",
"chars": 2123,
"preview": "//\n// GeneratorInfo.swift\n// Toucan\n//\n// Created by Viasz-Kádi Ferenc on 2025. 03. 19..\n//\n\nimport _GitCommitHash\nim"
},
{
"path": "Sources/ToucanCore/Logger.swift",
"chars": 545,
"preview": "//\n// Logger.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 18..\n//\n\nimport Logging\n\n/// A protocol for t"
},
{
"path": "Sources/ToucanCore/ToucanError.swift",
"chars": 6032,
"preview": "//\n// ToucanError.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 20..\n//\n\nimport Foundation\n\n/// A protoc"
},
{
"path": "Sources/ToucanMarkdown/Markdown/HTML.swift",
"chars": 1074,
"preview": "//\n// HTML.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 02. 19..\n//\n\nstruct HTML {\n enum TagType {\n "
},
{
"path": "Sources/ToucanMarkdown/Markdown/HTMLVisitor.swift",
"chars": 14845,
"preview": "//\n// HTMLVisitor.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 02. 19..\n//\n\nimport Logging\nimport Markdown\n"
},
{
"path": "Sources/ToucanMarkdown/Markdown/MarkdownBlockDirective.swift",
"chars": 3913,
"preview": "//\n// MarkdownBlockDirective.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 02. 19..\n//\n\n/// A representation"
},
{
"path": "Sources/ToucanMarkdown/Markdown/MarkdownToHTMLRenderer.swift",
"chars": 2593,
"preview": "//\n// MarkdownToHTMLRenderer.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 02. 19..\n//\n\nimport Logging\nimpor"
},
{
"path": "Sources/ToucanMarkdown/MarkdownRenderer.swift",
"chars": 7670,
"preview": "//\n// MarkdownRenderer.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 02. 20..\n//\n\nimport Logging\nimport Touc"
},
{
"path": "Sources/ToucanMarkdown/Outline/Outline.swift",
"chars": 1368,
"preview": "//\n// Outline.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 04. 17..\n//\n\n/// A hierarchical representation o"
},
{
"path": "Sources/ToucanMarkdown/Outline/OutlineParser.swift",
"chars": 3271,
"preview": "//\n// OutlineParser.swift\n// Toucan\n//\n// Created by Viasz-Kádi Ferenc on 2024. 10. 14..\n//\n\nimport Logging\nimport Sw"
},
{
"path": "Sources/ToucanMarkdown/ReadingTime/ReadingTimeCalculator.swift",
"chars": 1317,
"preview": "//\n// ReadingTimeCalculator.swift\n// Toucan\n//\n// Created by Viasz-Kádi Ferenc on 2024. 10. 15..\n//\n\nimport Logging\ni"
},
{
"path": "Sources/ToucanMarkdown/Transformers/ContentTransformer.swift",
"chars": 871,
"preview": "//\n// ContentTransformer.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 02. 21..\n//\n\n/// Represents a content"
},
{
"path": "Sources/ToucanMarkdown/Transformers/TransformerExecutor.swift",
"chars": 3857,
"preview": "//\n// TransformerExecutor.swift\n// Toucan\n//\n// Created by Viasz-Kádi Ferenc on 2024. 10. 15..\n//\n\nimport Foundation\n"
},
{
"path": "Sources/ToucanMarkdown/Transformers/TransformerPipeline.swift",
"chars": 1167,
"preview": "//\n// TransformerPipeline.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 02. 21..\n//\n\n/// Represents a sequen"
},
{
"path": "Sources/ToucanSDK/Behaviors/Behavior.swift",
"chars": 607,
"preview": "//\n// Behavior.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 06. 16..\n//\n\nimport struct Foundation.URL\n\n/// "
},
{
"path": "Sources/ToucanSDK/Behaviors/CompileSASSBehavior.swift",
"chars": 1148,
"preview": "//\n// CompileSASSBehavior.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 12..\n//\n\nimport DartSass\nimport "
},
{
"path": "Sources/ToucanSDK/Behaviors/MinifyCSSBehavior.swift",
"chars": 457,
"preview": "//\n// MinifyCSSBehavior.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 12..\n//\n\nimport Foundation\nimport "
},
{
"path": "Sources/ToucanSDK/Content/Content+Query.swift",
"chars": 10363,
"preview": "//\n// Content+Query.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 01. 31..\n//\n\nimport Foundation\nimport Touc"
},
{
"path": "Sources/ToucanSDK/Content/Content.swift",
"chars": 2828,
"preview": "//\n// Content.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 01. 15..\n//\n\nimport ToucanSource\n\n/// Represents"
},
{
"path": "Sources/ToucanSDK/Content/ContentResolver.swift",
"chars": 27307,
"preview": "//\n// ContentResolver.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 21..\n//\n\nimport Foundation\nimport Lo"
},
{
"path": "Sources/ToucanSDK/Content/ContentTypeResolver.swift",
"chars": 2053,
"preview": "//\n// ContentTypeResolver.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 30..\n//\n\nimport ToucanCore\nimpor"
},
{
"path": "Sources/ToucanSDK/Content/IteratorInfo.swift",
"chars": 2482,
"preview": "//\n// IteratorInfo.swift\n// Toucan\n//\n// Created by gerp83 on 2025. 04. 17..\n//\n\n/// Provides pagination and iteratio"
},
{
"path": "Sources/ToucanSDK/Content/Query+Resolve.swift",
"chars": 2194,
"preview": "//\n// Query+Resolve.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 16..\n//\n\nimport ToucanSource\n\npublic e"
},
{
"path": "Sources/ToucanSDK/Content/RelationValue.swift",
"chars": 1304,
"preview": "//\n// RelationValue.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 01. 30..\n//\n\nimport ToucanSource\n\n/// Repr"
},
{
"path": "Sources/ToucanSDK/DateFormats/DateContext.swift",
"chars": 2808,
"preview": "//\n// DateContext.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 02. 12..\n//\n\n/// A configuration container f"
},
{
"path": "Sources/ToucanSDK/DateFormats/ToucanDateFormatters.swift",
"chars": 9753,
"preview": "//\n// ToucanDateFormatters.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 26..\n//\n\nimport Foundation\nimpo"
},
{
"path": "Sources/ToucanSDK/Models/ContextBundle.swift",
"chars": 1440,
"preview": "//\n// ContextBundle.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 02. 21..\n//\n\nimport ToucanSource\n\n/// A bu"
},
{
"path": "Sources/ToucanSDK/Models/Destination.swift",
"chars": 982,
"preview": "//\n// Destination.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 02. 21..\n//\n\n/// Represents the destination "
},
{
"path": "Sources/ToucanSDK/Models/PipelineResult.swift",
"chars": 1844,
"preview": "//\n// PipelineResult.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 02. 21..\n//\n\n/// Represents the output of"
},
{
"path": "Sources/ToucanSDK/Models/Slug.swift",
"chars": 3022,
"preview": "//\n// Slug.swift\n// Toucan\n//\n// Created by gerp83 on 2025. 04. 17..\n//\n\n/// A value type representing a URL-friendly"
},
{
"path": "Sources/ToucanSDK/Outputs/ContextBundleToHTMLRenderer.swift",
"chars": 2779,
"preview": "//\n// ContextBundleToHTMLRenderer.swift\n// Toucan\n//\n// Created by Viasz-Kádi Ferenc on 2025. 05. 13..\n//\n\nimport Fou"
},
{
"path": "Sources/ToucanSDK/Outputs/ContextBundleToJSONRenderer.swift",
"chars": 3327,
"preview": "//\n// ContextBundleToJSONRenderer.swift\n// Toucan\n//\n// Created by Viasz-Kádi Ferenc on 2025. 05. 13..\n//\n\nimport Fou"
},
{
"path": "Sources/ToucanSDK/Renderers/BuildTargetSourceRenderer.swift",
"chars": 25316,
"preview": "//\n// BuildTargetSourceRenderer.swift\n// Toucan\n//\n// Created by Viasz-Kádi Ferenc on 2025. 03. 25..\n//\n\nimport Found"
},
{
"path": "Sources/ToucanSDK/Renderers/MustacheRenderer.swift",
"chars": 2333,
"preview": "//\n// MustacheRenderer.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 02. 16..\n//\n\nimport Foundation\nimport L"
},
{
"path": "Sources/ToucanSDK/Toucan.swift",
"chars": 11437,
"preview": "//\n// Toucan.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 04. 17..\n//\n\nimport FileManagerKit\nimport Foundat"
},
{
"path": "Sources/ToucanSDK/Utilities/Any+AnyCodable.swift",
"chars": 2092,
"preview": "//\n// Any+AnyCodable.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 03. 06..\n//\n\nimport ToucanSource\n\n/// Rec"
},
{
"path": "Sources/ToucanSDK/Utilities/AnyCodable+Json.swift",
"chars": 1012,
"preview": "//\n// AnyCodable+Json.swift\n// Toucan\n//\n// Created by Viasz-Kádi Ferenc on 2025. 05. 12..\n//\n\nimport Foundation\nimpo"
},
{
"path": "Sources/ToucanSDK/Utilities/Array+AnyCodable.swift",
"chars": 538,
"preview": "//\n// Array+AnyCodable.swift\n// Toucan\n//\n// Created by Viasz-Kádi Ferenc on 2025. 05. 11..\n//\n\nimport Foundation\nimp"
},
{
"path": "Sources/ToucanSDK/Utilities/ContextKeys.swift",
"chars": 1087,
"preview": "//\n// ContextKeys.swift\n// Toucan\n//\n// Created by Viasz-Kádi Ferenc on 2025. 09. 04..\n//\n\n/// Root-level keys used i"
},
{
"path": "Sources/ToucanSDK/Utilities/CopyManager.swift",
"chars": 1404,
"preview": "//\n// CopyManager.swift\n// Toucan\n//\n// Created by gerp83 on 2025. 04. 17..\n//\n\nimport FileManagerKit\nimport Foundati"
},
{
"path": "Sources/ToucanSDK/Utilities/Dictionary+AnyCodable.swift",
"chars": 589,
"preview": "//\n// Dictionary+AnyCodable.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 01. 31..\n//\n\nimport Foundation\nimp"
},
{
"path": "Sources/ToucanSDK/Utilities/Encodable+Json.swift",
"chars": 752,
"preview": "//\n// Encodable+Json.swift\n// Toucan\n//\n// Created by Viasz-Kádi Ferenc on 2025. 05. 11..\n//\n\nimport Foundation\n\npubl"
},
{
"path": "Sources/ToucanSDK/Utilities/FirstSucceeding.swift",
"chars": 855,
"preview": "//\n// FirstSucceeding.swift\n// Toucan\n//\n// Created by Viasz-Kádi Ferenc on 2025. 05. 11..\n//\n\n/// Attempts to execut"
},
{
"path": "Sources/ToucanSDK/Validators/BuildTargetSourceValidator.swift",
"chars": 6625,
"preview": "//\n// BuildTargetSourceValidator.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 23..\n//\n\nimport Foundatio"
},
{
"path": "Sources/ToucanSDK/Validators/TemplateValidator.swift",
"chars": 1940,
"preview": "//\n// TemplateValidator.swift\n// Toucan\n//\n// Created by Viasz-Kádi Ferenc on 2025. 06. 17..\n//\n\nimport Foundation\nim"
},
{
"path": "Sources/ToucanSerialization/ToucanDecoder.swift",
"chars": 2222,
"preview": "//\n// ToucanDecoder.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 01. 29..\n//\n\nimport struct Foundation.Data"
},
{
"path": "Sources/ToucanSerialization/ToucanDecoderError.swift",
"chars": 2878,
"preview": "//\n// ToucanDecoderError.swift\n// Toucan\n//\n// Created by gerp83 on 2025. 04. 17..\n//\n\nimport ToucanCore\n\n/// Extensi"
},
{
"path": "Sources/ToucanSerialization/ToucanEncoder.swift",
"chars": 1873,
"preview": "//\n// ToucanEncoder.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 03. 06..\n//\n\nimport struct Foundation.Data"
},
{
"path": "Sources/ToucanSerialization/ToucanEncoderError.swift",
"chars": 1726,
"preview": "//\n// ToucanEncoderError.swift\n// Toucan\n//\n// Created by gerp83 on 2025. 04. 17..\n//\n\nimport ToucanCore\n\n/// Extensi"
},
{
"path": "Sources/ToucanSerialization/ToucanJSONDecoder.swift",
"chars": 1125,
"preview": "//\n// ToucanJSONDecoder.swift\n// Toucan\n//\n// Created by Binary Birds on 2025. 04. 17..\n\nimport struct Foundation.Dat"
},
{
"path": "Sources/ToucanSerialization/ToucanJSONEncoder.swift",
"chars": 1621,
"preview": "//\n// ToucanJSONEncoder.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 18..\n//\n\nimport struct Foundation."
},
{
"path": "Sources/ToucanSerialization/ToucanYAMLDecoder.swift",
"chars": 1038,
"preview": "//\n// ToucanYAMLDecoder.swift\n// Toucan\n//\n// Created by Binary Birds on 2025. 04. 17..\n\nimport struct Foundation.Dat"
},
{
"path": "Sources/ToucanSerialization/ToucanYAMLEncoder.swift",
"chars": 954,
"preview": "//\n// ToucanYAMLEncoder.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 03. 06..\n//\n\nimport struct Foundation."
},
{
"path": "Sources/ToucanSource/Errors/ObjectLoaderError.swift",
"chars": 1273,
"preview": "//\n// ObjectLoaderError.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 21..\n//\n\nimport struct Foundation."
},
{
"path": "Sources/ToucanSource/Errors/SourceLoaderError.swift",
"chars": 1357,
"preview": "//\n// SourceLoaderError.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 21..\n//\n\nimport ToucanCore\n\n/// A "
},
{
"path": "Sources/ToucanSource/Errors/TemplateLoaderError.swift",
"chars": 1372,
"preview": "//\n// TemplateLoaderError.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 21..\n//\n\nimport ToucanCore\n\n/// "
},
{
"path": "Sources/ToucanSource/Extensions/Decoder+Validate.swift",
"chars": 1921,
"preview": "//\n// Decoder+Validate.swift\n// Toucan\n//\n// Created by Ferenc Viasz-Kadi on 2025. 08. 19..\n//\n\nextension Decoder {\n\n"
},
{
"path": "Sources/ToucanSource/Extensions/Dictionary+AnyCodable.swift",
"chars": 1765,
"preview": "//\n// Dictionary+AnyCodable.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 18..\n//\n\nimport struct Foundat"
},
{
"path": "Sources/ToucanSource/Extensions/FileManagerKit+Extensions.swift",
"chars": 2885,
"preview": "//\n// FileManagerKit+Extensions.swift\n// Toucan\n//\n// Created by Binary Birds on 2025. 04. 17..\n\nimport FileManagerKi"
},
{
"path": "Sources/ToucanSource/Loaders/BuildTargetSourceLoader.swift",
"chars": 9836,
"preview": "//\n// BuildTargetSourceLoader.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 04. 04..\n//\n\nimport FileManagerK"
},
{
"path": "Sources/ToucanSource/Loaders/ObjectLoader.swift",
"chars": 4049,
"preview": "//\n// ObjectLoader.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 16..\n//\n\nimport Foundation\nimport Loggi"
},
{
"path": "Sources/ToucanSource/Loaders/RawContentLoader.swift",
"chars": 12090,
"preview": "//\n// RawContentLoader.swift\n// Toucan\n//\n// Created by Viasz-Kádi Ferenc on 2025. 03. 03..\n//\n\nimport FileManagerKit"
},
{
"path": "Sources/ToucanSource/Loaders/TemplateLoader.swift",
"chars": 5618,
"preview": "//\n// TemplateLoader.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 21..\n//\n\nimport FileManagerKit\nimport"
},
{
"path": "Sources/ToucanSource/MarkdownParser.swift",
"chars": 3017,
"preview": "//\n// MarkdownParser.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 04. 17..\n//\n\nimport Logging\nimport Toucan"
},
{
"path": "Sources/ToucanSource/Models/BuildTargetSource.swift",
"chars": 2502,
"preview": "//\n// BuildTargetSource.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 01. 31..\n//\n\nimport struct Foundation."
},
{
"path": "Sources/ToucanSource/Models/BuiltTargetSourceLocations.swift",
"chars": 4608,
"preview": "//\n// BuiltTargetSourceLocations.swift\n// Toucan\n//\n// Created by Viasz-Kádi Ferenc on 2025. 03. 01..\n//\n\nimport stru"
},
{
"path": "Sources/ToucanSource/Models/Markdown.swift",
"chars": 1089,
"preview": "//\n// Markdown.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 18..\n//\n\n/// A representation of a Markdown"
},
{
"path": "Sources/ToucanSource/Models/Origin.swift",
"chars": 940,
"preview": "//\n// Origin.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 01. 30..\n//\n\n/// Represents the source origin of "
},
{
"path": "Sources/ToucanSource/Models/Path.swift",
"chars": 2833,
"preview": "//\n// Path.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 06. 04..\n//\n\nimport Foundation\n\n/// A value type re"
},
{
"path": "Sources/ToucanSource/Models/RawContent.swift",
"chars": 1691,
"preview": "//\n// RawContent.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 01. 30..\n//\n\n/// Represents the raw, unproces"
},
{
"path": "Sources/ToucanSource/Models/Template.swift",
"chars": 6358,
"preview": "//\n// Template.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 21..\n//\n\nimport struct Foundation.URL\nimpor"
},
{
"path": "Sources/ToucanSource/Models/View.swift",
"chars": 929,
"preview": "//\n// View.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 01. 31..\n//\n\nimport Foundation\n\n/// Represents the "
},
{
"path": "Sources/ToucanSource/Objects/AnyCodable.swift",
"chars": 9883,
"preview": "//\n// AnyCodable.swift\n// Toucan\n//\n// Created by Binary Birds on 2025. 04. 17..\n\nimport Foundation\n\n// public protoc"
},
{
"path": "Sources/ToucanSource/Objects/Blocks/Block+Attribute.swift",
"chars": 815,
"preview": "//\n// Block+Attribute.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 18..\n//\n\npublic extension Block {\n "
},
{
"path": "Sources/ToucanSource/Objects/Blocks/Block+Parameter.swift",
"chars": 1179,
"preview": "//\n// Block+Parameter.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 18..\n//\n\npublic extension Block {\n "
},
{
"path": "Sources/ToucanSource/Objects/Blocks/Block.swift",
"chars": 2102,
"preview": "//\n// Block.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 17..\n//\n\n/// A representation of a custom bloc"
},
{
"path": "Sources/ToucanSource/Objects/Config/Config+Blocks.swift",
"chars": 1621,
"preview": "//\n// Config+Blocks.swift\n// Toucan\n//\n// Created by Viasz-Kádi Ferenc on 2025. 04. 18..\n//\n\npublic extension Config "
},
{
"path": "Sources/ToucanSource/Objects/Config/Config+Contents.swift",
"chars": 2284,
"preview": "//\n// Config+Contents.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 02. 21..\n//\n\npublic extension Config {\n "
},
{
"path": "Sources/ToucanSource/Objects/Config/Config+DataTypes+Date.swift",
"chars": 3082,
"preview": "//\n// Config+DataTypes+Date.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 02. 21..\n//\n\npublic extension Conf"
},
{
"path": "Sources/ToucanSource/Objects/Config/Config+DataTypes.swift",
"chars": 1797,
"preview": "//\n// Config+DataTypes.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 02. 16..\n//\n\npublic extension Config {\n"
},
{
"path": "Sources/ToucanSource/Objects/Config/Config+Location.swift",
"chars": 574,
"preview": "//\n// Config+Location.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 02. 21..\n//\n\npublic extension Config {\n "
},
{
"path": "Sources/ToucanSource/Objects/Config/Config+Pipelines.swift",
"chars": 1648,
"preview": "//\n// Config+Pipelines.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 02. 21..\n//\n\npublic extension Config {\n"
},
{
"path": "Sources/ToucanSource/Objects/Config/Config+Renderer+ParagraphStyles.swift",
"chars": 2232,
"preview": "//\n// Config+Renderer+ParagraphStyles.swift\n// Toucan\n//\n// Created by gerp83 on 2025. 04. 17..\n//\n\npublic extension "
},
{
"path": "Sources/ToucanSource/Objects/Config/Config+RendererConfig.swift",
"chars": 3280,
"preview": "//\n// Config+RendererConfig.swift\n// Toucan\n//\n// Created by gerp83 on 2025. 03. 28..\n//\n\npublic extension Config {\n "
},
{
"path": "Sources/ToucanSource/Objects/Config/Config+Site.swift",
"chars": 2084,
"preview": "//\n// Config+Site.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 02. 21..\n//\n\npublic extension Config {\n /"
},
{
"path": "Sources/ToucanSource/Objects/Config/Config+Templates.swift",
"chars": 3528,
"preview": "//\n// Config+Templates.swift\n// Toucan\n//\n// Created by Viasz-Kádi Ferenc on 2025. 03. 01..\n//\n\nimport Foundation\n\npu"
},
{
"path": "Sources/ToucanSource/Objects/Config/Config+Types.swift",
"chars": 1637,
"preview": "//\n// Config+Types.swift\n// Toucan\n//\n// Created by Viasz-Kádi Ferenc on 2025. 04. 18..\n//\n\npublic extension Config {"
},
{
"path": "Sources/ToucanSource/Objects/Config/Config.swift",
"chars": 4867,
"preview": "//\n// Config.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 01. 29..\n//\n\nimport Foundation\n\n/// Represents th"
},
{
"path": "Sources/ToucanSource/Objects/Date/DateFormatterConfig.swift",
"chars": 3012,
"preview": "//\n// DateFormatterConfig.swift\n// Toucan\n//\n// Created by Viasz-Kádi Ferenc on 2025. 03. 28..\n//\n\n/// A configuratio"
},
{
"path": "Sources/ToucanSource/Objects/Date/DateLocalization.swift",
"chars": 3957,
"preview": "//\n// DateLocalization.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 28..\n//\n\nimport struct Foundation.L"
},
{
"path": "Sources/ToucanSource/Objects/Pipeline/Pipeline+Assets.swift",
"chars": 8060,
"preview": "//\n// Pipeline+Assets.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 04. 19..\n//\n\npublic extension Pipeline {"
},
{
"path": "Sources/ToucanSource/Objects/Pipeline/Pipeline+ContentTypes.swift",
"chars": 4480,
"preview": "//\n// Pipeline+ContentTypes.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 02. 03..\n//\n\npublic extension Pipe"
},
{
"path": "Sources/ToucanSource/Objects/Pipeline/Pipeline+DataTypes+Date.swift",
"chars": 2458,
"preview": "//\n// Pipeline+DataTypes+Date.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 30..\n//\n\npublic extension Pi"
},
{
"path": "Sources/ToucanSource/Objects/Pipeline/Pipeline+DataTypes.swift",
"chars": 1801,
"preview": "//\n// Pipeline+DataTypes.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 30..\n//\n\npublic extension Pipelin"
},
{
"path": "Sources/ToucanSource/Objects/Pipeline/Pipeline+Engine.swift",
"chars": 1920,
"preview": "//\n// Pipeline+Engine.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 02. 03..\n//\n\npublic extension Pipeline {"
},
{
"path": "Sources/ToucanSource/Objects/Pipeline/Pipeline+Output.swift",
"chars": 2076,
"preview": "//\n// Pipeline+Output.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 02. 16..\n//\n\npublic extension Pipeline {"
},
{
"path": "Sources/ToucanSource/Objects/Pipeline/Pipeline+Scope+Context.swift",
"chars": 6030,
"preview": "//\n// Pipeline+Scope+Context.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 02. 03..\n//\n\npublic extension Pip"
},
{
"path": "Sources/ToucanSource/Objects/Pipeline/Pipeline+Scope.swift",
"chars": 3756,
"preview": "//\n// Pipeline+Scope.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 02. 03..\n//\n\npublic extension Pipeline {\n"
},
{
"path": "Sources/ToucanSource/Objects/Pipeline/Pipeline+Transformers+Transformer.swift",
"chars": 1834,
"preview": "//\n// Pipeline+Transformers+Transformer.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 16..\n//\n\nimport Fo"
},
{
"path": "Sources/ToucanSource/Objects/Pipeline/Pipeline+Transformers.swift",
"chars": 1276,
"preview": "//\n// Pipeline+Transformers.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 02. 21..\n//\n\npublic extension Pipe"
},
{
"path": "Sources/ToucanSource/Objects/Pipeline/Pipeline.swift",
"chars": 6239,
"preview": "//\n// Pipeline.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 01. 16..\n//\n\n/// Represents a full content tran"
},
{
"path": "Sources/ToucanSource/Objects/Property/Property.swift",
"chars": 3033,
"preview": "//\n// Property.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 01. 21..\n//\n\n/// Represents a single content pr"
},
{
"path": "Sources/ToucanSource/Objects/Property/PropertyType.swift",
"chars": 3544,
"preview": "//\n// PropertyType.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 01. 21..\n//\n\n/// Represents the type of a c"
},
{
"path": "Sources/ToucanSource/Objects/Property/SystemPropertyKeys.swift",
"chars": 501,
"preview": "//\n// SystemPropertyKeys.swift\n// Toucan\n//\n// Created by Ferenc Viasz-Kadi on 2025. 08. 22..\n//\n\n/// Represents pred"
},
{
"path": "Sources/ToucanSource/Objects/Query/Condition.swift",
"chars": 3014,
"preview": "//\n// Condition.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 01. 21..\n//\n\n/// Represents a logical conditio"
},
{
"path": "Sources/ToucanSource/Objects/Query/Direction.swift",
"chars": 493,
"preview": "//\n// Direction.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 01. 21..\n//\n\n/// Represents the direction for "
},
{
"path": "Sources/ToucanSource/Objects/Query/Operator.swift",
"chars": 926,
"preview": "//\n// Operator.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 01. 21..\n//\n\n/// Represents a comparison or fil"
},
{
"path": "Sources/ToucanSource/Objects/Query/Order.swift",
"chars": 2112,
"preview": "//\n// Order.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 01. 15..\n//\n\n/// Represents a sorting rule for ord"
},
{
"path": "Sources/ToucanSource/Objects/Query/Query.swift",
"chars": 3132,
"preview": "//\n// Query.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 01. 15..\n//\n\n/// Represents a content query used t"
},
{
"path": "Sources/ToucanSource/Objects/Relation/Relation.swift",
"chars": 2405,
"preview": "//\n// Relation.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 01. 21..\n//\n\n/// Represents a relationship betw"
},
{
"path": "Sources/ToucanSource/Objects/Relation/RelationType.swift",
"chars": 453,
"preview": "//\n// RelationType.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 01. 21..\n//\n\n/// Defines the cardinality of"
},
{
"path": "Sources/ToucanSource/Objects/Settings/Settings.swift",
"chars": 2201,
"preview": "//\n// Settings.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 02. 11..\n//\n\n/// A custom coding key type for e"
},
{
"path": "Sources/ToucanSource/Objects/Target/Target.swift",
"chars": 4053,
"preview": "//\n// Target.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 15..\n//\n\n/// Represents a deployment target c"
},
{
"path": "Sources/ToucanSource/Objects/Target/TargetConfig.swift",
"chars": 2805,
"preview": "//\n// TargetConfig.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 15..\n//\n\n/// A structure that holds a l"
},
{
"path": "Sources/ToucanSource/Objects/Types/ContentType.swift",
"chars": 4696,
"preview": "//\n// ContentType.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 01. 15..\n//\n\n/// Describes a content type de"
},
{
"path": "Sources/_GitCommitHash/git_commit_hash.c",
"chars": 97,
"preview": "#include \"git_commit_hash.h\"\n\nconst char * git_commit_hash(void)\n{\n return GIT_COMMIT_HASH;\n}\n"
},
{
"path": "Sources/_GitCommitHash/include/git_commit_hash.h",
"chars": 110,
"preview": "#if !defined(GIT_COMMIT_HASH_H)\n#define GIT_COMMIT_HASH_H\n\nextern const char * git_commit_hash(void);\n\n#endif\n"
},
{
"path": "Sources/toucan/Entrypoint.swift",
"chars": 3058,
"preview": "//\n// Entrypoint.swift\n// Toucan\n//\n// Created by Binary Birds on 2025. 04. 15..\n\nimport ArgumentParser\nimport Dispat"
},
{
"path": "Sources/toucan-generate/Entrypoint.swift",
"chars": 1691,
"preview": "//\n// Entrypoint.swift\n// Toucan\n//\n// Created by Binary Birds on 2025. 04. 15..\n\nimport ArgumentParser\nimport Loggin"
},
{
"path": "Sources/toucan-init/Download.swift",
"chars": 2303,
"preview": "//\n// Download.swift\n// Toucan\n//\n// Created by Binary Birds on 2025. 03. 31..\n\nimport FileManagerKit\nimport Foundati"
},
{
"path": "Sources/toucan-init/Entrypoint.swift",
"chars": 2449,
"preview": "//\n// Entrypoint.swift\n// Toucan\n//\n// Created by Binary Birds on 2025. 04. 15..\n\nimport ArgumentParser\nimport FileMa"
},
{
"path": "Sources/toucan-serve/Entrypoint.swift",
"chars": 1835,
"preview": "//\n// Entrypoint.swift\n// Toucan\n//\n// Created by Binary Birds on 2025. 04. 15..\n\nimport ArgumentParser\nimport Founda"
},
{
"path": "Sources/toucan-serve/NotFoundMiddleware.swift",
"chars": 786,
"preview": "//\n// NotFoundMiddleware.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 01. 23..\n//\n\nimport Hummingbird\n\nstru"
},
{
"path": "Sources/toucan-watch/Entrypoint.swift",
"chars": 4463,
"preview": "//\n// Entrypoint.swift\n// Toucan\n//\n// Created by Binary Birds on 2025. 04. 15..\n\nimport ArgumentParser\nimport FileMo"
},
{
"path": "Tests/ToucanCoreTests/Extensions/StringExtensionsTestSuite.swift",
"chars": 3288,
"preview": "//\n// StringExtensionsTestSuite.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 17..\n//\n\nimport Testing\n\n@"
},
{
"path": "Tests/ToucanCoreTests/Extensions/URLExtensionsTestSuite.swift",
"chars": 876,
"preview": "//\n// URLExtensionsTestSuite.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 17..\n//\n\nimport Foundation\nim"
},
{
"path": "Tests/ToucanCoreTests/ToucanCoreTestSuite.swift",
"chars": 456,
"preview": "//\n// ToucanCoreTestSuite.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 17..\n//\n\nimport Testing\n@testabl"
},
{
"path": "Tests/ToucanMarkdownTests/ContentRendererTestSuite.swift",
"chars": 1634,
"preview": "//\n// ContentRendererTestSuite.swift\n// Toucan\n//\n// Created by Binary Birds on 2025. 04. 15..\n\nimport Foundation\nimp"
},
{
"path": "Tests/ToucanMarkdownTests/HTMLVisitorTestSuite.swift",
"chars": 18340,
"preview": "//\n// HTMLVisitorTestSuite.swift\n// Toucan\n//\n// Created by Binary Birds on 2025. 04. 15..\n\nimport Logging\nimport Mar"
},
{
"path": "Tests/ToucanMarkdownTests/MarkdownBlockDirective+Mock.swift",
"chars": 1764,
"preview": "//\n// MarkdownBlockDirective+Mock.swift\n// Toucan\n//\n// Created by Binary Birds on 2025. 04. 17..\n\nimport Foundation\n"
},
{
"path": "Tests/ToucanMarkdownTests/MarkdownBlockDirectiveTestSuite.swift",
"chars": 5830,
"preview": "//\n// MarkdownBlockDirectiveTestSuite.swift\n// Toucan\n//\n// Created by Binary Birds on 2025. 04. 17..\n\nimport Logging"
},
{
"path": "Tests/ToucanMarkdownTests/OutlineTestSuite.swift",
"chars": 1546,
"preview": "//\n// OutlineTestSuite.swift\n// Toucan\n//\n// Created by Binary Birds on 2025. 02. 20..\n\nimport Testing\n\n@testable imp"
},
{
"path": "Tests/ToucanSDKTests/BuildTargetSource/BuildTargetSourceRendererTestSuite.swift",
"chars": 32327,
"preview": "//\n// BuildTargetSourceRendererTestSuite.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 06. 09..\n//\n\nimport F"
},
{
"path": "Tests/ToucanSDKTests/BuildTargetSource/BuildTargetSourceValidatorTestSuite.swift",
"chars": 3843,
"preview": "//\n// BuildTargetSourceValidatorTestSuite.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 23..\n//\n//\nimpor"
},
{
"path": "Tests/ToucanSDKTests/Content/ContentQueryTestSuite.swift",
"chars": 28398,
"preview": "//\n// ContentQueryTestSuite.swift\n// Toucan\n//\n// Created by Binary Birds on 2025. 04. 15..\n//\nimport Foundation\nimpo"
},
{
"path": "Tests/ToucanSDKTests/Content/ContentResolverTestSuite.swift",
"chars": 42461,
"preview": "//\n// ContentResolverTestSuite.swift\n// Toucan\n//\n// Created by Viasz-Kádi Ferenc on 2025. 02. 20..\n//\n//\nimport Foun"
},
{
"path": "Tests/ToucanSDKTests/DateFormatter/ToucanDateFormatterTestSuite.swift",
"chars": 2337,
"preview": "//\n// ToucanDateFormatterTestSuite.swift\n// Toucan\n//\n// Created by Binary Birds on 2025. 04. 17..\n//\n\nimport Foundat"
},
{
"path": "Tests/ToucanSDKTests/E2ETestSuite.swift",
"chars": 63257,
"preview": "//\n// E2ETestSuite.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 06. 11..\n//\n\nimport FileManagerKitBuilder\ni"
},
{
"path": "Tests/ToucanSDKTests/Files/MarkdownFile.swift",
"chars": 1147,
"preview": "//\n// MarkdownFile.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 06. 04..\n//\n\nimport FileManagerKit\nimport F"
},
{
"path": "Tests/ToucanSDKTests/Files/MustacheFile.swift",
"chars": 681,
"preview": "//\n// MustacheFile.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 06. 04..\n//\n\nimport FileManagerKit\nimport F"
},
{
"path": "Tests/ToucanSDKTests/Files/RawContentBundle.swift",
"chars": 914,
"preview": "//\n// RawContentBundle.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 06. 04..\n//\n\nimport FileManagerKit\nimpo"
},
{
"path": "Tests/ToucanSDKTests/Files/YAMLFile.swift",
"chars": 738,
"preview": "//\n// YAMLFile.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 06. 04..\n//\n\nimport FileManagerKit\nimport FileM"
},
{
"path": "Tests/ToucanSDKTests/Mocks/Mocks+Blocks.swift",
"chars": 2359,
"preview": "//\n// Mocks+Blocks.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 21..\n//\n\nimport ToucanSource\n\nextension"
},
{
"path": "Tests/ToucanSDKTests/Mocks/Mocks+BuildTargetSources.swift",
"chars": 6009,
"preview": "//\n// Mocks+BuildTargetSources.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 21..\n//\n\nimport Foundation\n"
},
{
"path": "Tests/ToucanSDKTests/Mocks/Mocks+ContentTypes.swift",
"chars": 11747,
"preview": "//\n// Mocks+ContentTypes.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 21..\n//\n\nimport ToucanSource\n\next"
},
{
"path": "Tests/ToucanSDKTests/Mocks/Mocks+E2E.swift",
"chars": 19916,
"preview": "//\n// Mocks+E2E.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 06. 08..\n//\n\nimport FileManagerKitBuilder\nimpo"
},
{
"path": "Tests/ToucanSDKTests/Mocks/Mocks+Files.swift",
"chars": 9224,
"preview": "//\n// Mocks+Files.swift\n// Toucan\n//\n// Created by gerp83 on 2025. 04. 15..\n//\n\nimport FileManagerKitBuilder\nimport F"
},
{
"path": "Tests/ToucanSDKTests/Mocks/Mocks+Pipelines.swift",
"chars": 14854,
"preview": "//\n// Mocks+Pipelines.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 21..\n//\n\nimport ToucanSource\nimport "
},
{
"path": "Tests/ToucanSDKTests/Mocks/Mocks+RawContents.swift",
"chars": 11625,
"preview": "//\n// Mocks+RawContents.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 21..\n//\n\nimport Foundation\nimport "
},
{
"path": "Tests/ToucanSDKTests/Mocks/Mocks+Templates.swift",
"chars": 2795,
"preview": "//\n// Mocks+Templates.swift\n// Toucan\n//\n// Created by Viasz-Kádi Ferenc on 2025. 06. 17..\n//\n\nimport ToucanCore\n@tes"
},
{
"path": "Tests/ToucanSDKTests/Mocks/Mocks+Views.swift",
"chars": 22560,
"preview": "//\n// Mocks+Views.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 21..\n//\n\n@testable import ToucanSource\n\n"
},
{
"path": "Tests/ToucanSDKTests/Mocks/Mocks.swift",
"chars": 244,
"preview": "//\n// Mocks.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 01. 30..\n//\n\nenum Mocks {\n enum ContentTypes {}"
},
{
"path": "Tests/ToucanSDKTests/Template/TemplateValidatorTestSuite.swift",
"chars": 3943,
"preview": "//\n// TemplateValidatorTestSuite.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 23..\n//\n//\nimport Foundat"
},
{
"path": "Tests/ToucanSDKTests/Toucan/ToucanTestSuite.swift",
"chars": 2333,
"preview": "//\n// ToucanTestSuite.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 06. 20..\n//\n\nimport FileManagerKitBuilde"
},
{
"path": "Tests/ToucanSDKTests/Utilities/AnyCodableWrapTests.swift",
"chars": 2389,
"preview": "//\n// AnyCodableWrapTests.swift\n// Toucan\n//\n// Created by gerp83 on 2025. 04. 28..\n//\n\nimport Foundation\nimport Test"
},
{
"path": "Tests/ToucanSDKTests/Utilities/CopyManagerTestSuite.swift",
"chars": 2610,
"preview": "//\n// CopyManagerTestSuite.swift\n// Toucan\n//\n// Created by Viasz-Kádi Ferenc on 2025. 03. 04..\n//\n\nimport FileManage"
},
{
"path": "Tests/ToucanSDKTests/Utilities/PrettyPrint.swift",
"chars": 802,
"preview": "//\n// PrettyPrint.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 02. 11..\n//\n\nimport Foundation\nimport Toucan"
},
{
"path": "Tests/ToucanSDKTests/Utilities/RecursiveMergeTests.swift",
"chars": 1040,
"preview": "//\n// RecursiveMergeTests.swift\n// Toucan\n//\n// Created by Binary Birds on 2025. 04. 15..\n//\n\nimport Testing\nimport T"
},
{
"path": "Tests/ToucanSDKTests/Utilities/SlugTests.swift",
"chars": 603,
"preview": "//\n// SlugTests.swift\n// Toucan\n//\n// Created by gerp83 on 2025. 04. 04..\n//\n\nimport Testing\nimport ToucanSDK\n\n@Suite"
},
{
"path": "Tests/ToucanSDKTests/Utilities/UnboxingTestSuite.swift",
"chars": 9670,
"preview": "//\n// UnboxingTestSuite.swift\n// Toucan\n//\n// Created by Viasz-Kádi Ferenc on 2025. 05. 09..\n//\n//\n\nimport Foundation"
},
{
"path": "Tests/ToucanSourceTests/BuildTargetSourceLoaderTestSuite.swift",
"chars": 9821,
"preview": "//\n// BuildTargetSourceLoaderTestSuite.swift\n// Toucan\n//\n// Created by Binary Birds on 2025. 03. 04..\n\nimport FileMa"
},
{
"path": "Tests/ToucanSourceTests/Extensions/FileManagerKitExtensionsTestSuite.swift",
"chars": 2070,
"preview": "//\n// FileManagerKitExtensionsTestSuite.swift\n// Toucan\n//\n// Created by Binary Birds on 2025. 03. 04..\n\nimport FileM"
},
{
"path": "Tests/ToucanSourceTests/Files/YAMLFile.swift",
"chars": 738,
"preview": "//\n// YAMLFile.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 20..\n//\n\nimport FileManagerKit\nimport FileM"
},
{
"path": "Tests/ToucanSourceTests/MarkdownParserTestSuite.swift",
"chars": 5986,
"preview": "//\n// MarkdownParserTestSuite.swift\n// Toucan\n//\n// Created by Binary Birds on 2025. 04. 15..\n//\nimport Logging\nimpor"
},
{
"path": "Tests/ToucanSourceTests/Models/BuildTargetSourceLocationsTestSuite.swift",
"chars": 3203,
"preview": "//\n// BuildTargetSourceLocationsTestSuite.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 21..\n//\n\nimport "
},
{
"path": "Tests/ToucanSourceTests/Objects/AnyCodableTestSuite.swift",
"chars": 11086,
"preview": "//\n// AnyCodableTestSuite.swift\n// Toucan\n//\n// Created by Binary Birds on 2025. 03. 30..\n\nimport Foundation\nimport T"
},
{
"path": "Tests/ToucanSourceTests/Objects/Config/ConfigTestSuite.swift",
"chars": 5182,
"preview": "//\n// ConfigTestSuite.swift\n// Toucan\n//\n// Created by Binary Birds on 2025. 04. 17..\n\nimport Foundation\nimport Testi"
},
{
"path": "Tests/ToucanSourceTests/Objects/DateFormatting/DateFormattingTestSuite.swift",
"chars": 5937,
"preview": "//\n// DateFormattingTestSuite.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 28..\n//\n\nimport Foundation\ni"
},
{
"path": "Tests/ToucanSourceTests/Objects/Pipeline/PipelineContentTypeTestSuite.swift",
"chars": 1587,
"preview": "//\n// PipelineContentTypeTestSuite.swift\n// Toucan\n//\n// Created by Binary Birds on 2025. 03. 12..\n\nimport Foundation"
},
{
"path": "Tests/ToucanSourceTests/Objects/Pipeline/PipelineScopeContextTestSuite.swift",
"chars": 2173,
"preview": "//\n// PipelineScopeContextTestSuite.swift\n// Toucan\n//\n// Created by Binary Birds on 2025. 03. 12..\n\nimport Foundatio"
},
{
"path": "Tests/ToucanSourceTests/Objects/Pipeline/PipelineScopeTestSuite.swift",
"chars": 1699,
"preview": "//\n// PipelineScopeTestSuite.swift\n// Toucan\n//\n// Created by Binary Birds on 2025. 03. 12..\n\nimport Foundation\nimpor"
},
{
"path": "Tests/ToucanSourceTests/Objects/Pipeline/PipelineTestSuite.swift",
"chars": 6821,
"preview": "//\n// PipelineTestSuite.swift\n// Toucan\n//\n// Created by Binary Birds on 2025. 03. 30..\n\nimport Foundation\nimport Tes"
},
{
"path": "Tests/ToucanSourceTests/Objects/Pipeline/PipelineTransformersTestSuite.swift",
"chars": 794,
"preview": "//\n// PipelineTransformersTestSuite.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 18..\n//\n\nimport Founda"
},
{
"path": "Tests/ToucanSourceTests/Objects/Property/PropertyTestSuite.swift",
"chars": 2656,
"preview": "//\n// PropertyTestSuite.swift\n// Toucan\n//\n// Created by Binary Birds on 2025. 03. 30..\n\nimport Foundation\nimport Tes"
},
{
"path": "Tests/ToucanSourceTests/Objects/Property/PropertyTypeTestSuite.swift",
"chars": 5063,
"preview": "//\n// PropertyTypeTestSuite.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 01. 31..\n//\n\nimport Foundation\nimp"
},
{
"path": "Tests/ToucanSourceTests/Objects/Query/ConditionTestSuite.swift",
"chars": 9674,
"preview": "//\n// ConditionTestSuite.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 18..\n//\n\nimport Foundation\nimport"
},
{
"path": "Tests/ToucanSourceTests/Objects/Query/DirectionTestSuite.swift",
"chars": 1371,
"preview": "//\n// DirectionTestSuite.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 18..\n//\n\nimport Foundation\nimport"
},
{
"path": "Tests/ToucanSourceTests/Objects/Query/OperatorTestSuite.swift",
"chars": 3766,
"preview": "//\n// OperatorTestSuite.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 18..\n//\n\nimport Foundation\nimport "
},
{
"path": "Tests/ToucanSourceTests/Objects/Query/OrderTestSuite.swift",
"chars": 1497,
"preview": "//\n// OrderTestSuite.swift\n// Toucan\n//\n// Created by Tibor Bödecs on 2025. 05. 18..\n//\n\nimport Foundation\nimport Tes"
}
]
// ... and 17 more files (download for full content)
About this extraction
This page contains the full source code of the toucansites/toucan GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 217 files (908.3 KB), approximately 197.6k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.