[
  {
    "path": ".github/workflows/actions.yml",
    "content": "name: Actions\n\non:\n  pull_request:\n    branches:\n      - main\n\njobs:\n\n  bb_checks:\n    name: BB Checks\n    uses: BinaryBirds/github-workflows/.github/workflows/extra_soundness.yml@main\n    with:\n      local_swift_dependencies_check_enabled : true\n\n  swiftlang_checks:\n    name: Swiftlang Checks\n    uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main\n    with:\n      license_header_check_project_name: \"Toucan\"\n      format_check_enabled : true\n      broken_symlink_check_enabled : true\n      unacceptable_language_check_enabled : true\n      api_breakage_check_enabled : false\n      docs_check_enabled : false\n      license_header_check_enabled : false\n      shell_check_enabled : false\n      yamllint_check_enabled : false\n      python_lint_check_enabled : false\n\n  swiftlang_tests:\n    name: Swiftlang Tests\n    uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main\n    with:\n      enable_windows_checks : false\n      linux_build_command: \"swift test --parallel --enable-code-coverage\"\n      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\\\"}]\""
  },
  {
    "path": ".github/workflows/linux.yml",
    "content": "name: Build, Test and Upload Linux Binaries for tag\non:\n  workflow_call:\n    inputs:\n      version:\n        required: true\n        type: string\n      run_rpm:\n        required: false\n        type: boolean\n        default: true\n      run_deb:\n        required: false\n        type: boolean\n        default: true\n      static_stdlib:\n        required: false\n        type: boolean\n        default: true\n\njobs:\n  precheck:\n    runs-on: ubuntu-latest\n    outputs:\n      should_run: ${{ steps.check.outputs.should_run }}\n    steps:\n      - id: check\n        run: |\n          if [[ \"${{ inputs.run_rpm }}\" == \"true\" || \"${{ inputs.run_deb }}\" == \"true\" ]]; then\n            echo \"✅ At least one packaging format enabled\"\n            echo \"should_run=true\" >> $GITHUB_OUTPUT\n          else\n            echo \"🚫 Both run_rpm and run_deb are false — skipping workflow\"\n            echo \"should_run=false\" >> $GITHUB_OUTPUT\n          fi\n\n  build-binaries:\n    needs: precheck\n    if: needs.precheck.outputs.should_run == 'true'\n    runs-on: ubuntu-latest\n    container:\n      image: swift:6.3\n    permissions:\n      contents: write\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Install required Swift tools\n        run: |\n          chmod +x ./scripts/packaging/*.sh\n          apt-get update\n          apt-get install -y curl git clang libcurl4-openssl-dev libssl-dev libatomic1 zip\n\n      - name: Install RPM tooling\n        if: inputs.run_rpm\n        run: apt-get install -y rpm\n\n      - name: Install DEB tooling\n        if: inputs.run_deb\n        run: apt-get install -y dpkg-dev\n\n      - name: Build with static stdlib\n        if: inputs.static_stdlib\n        run: |\n          echo \"🔧 Building with static Swift stdlib\"\n          swift build -c release -Xswiftc -static-stdlib\n      \n      - name: Build without static stdlib\n        if: ${{ !inputs.static_stdlib }}\n        run: |\n          echo \"🔧 Building without static Swift stdlib\"\n          swift build -c release\n\n      - name: Build RPM\n        if: inputs.run_rpm\n        run: ./scripts/packaging/rpm.sh ${{ inputs.version }}\n\n      - name: Verify RPM\n        if: inputs.run_rpm\n        run: |\n          RPM=\"build-rpm/toucan-linux-x86_64-${{ inputs.version }}.rpm\"\n          echo \"🧪 Verifying $RPM\"\n          rpm -Kv \"$RPM\"\n          rpm -qp \"$RPM\"\n          echo \"✅ RPM passed verification\"\n\n      - name: Build DEB\n        if: inputs.run_deb\n        run: ./scripts/packaging/deb.sh ${{ inputs.version }}\n\n      - name: Verify DEB\n        if: inputs.run_deb\n        run: |\n          DEB=\"build-deb/toucan-linux-amd64-${{ inputs.version }}.deb\"\n          echo \"🧪 Verifying $DEB\"\n          dpkg-deb --info \"$DEB\"\n          dpkg-deb --contents \"$DEB\"\n          echo \"✅ DEB passed verification\"\n\n      - name: Upload Linux artifacts\n        if: inputs.run_rpm || inputs.run_deb\n        uses: actions/upload-artifact@v4\n        with:\n          name: linux-artifacts\n          retention-days: 1     #no need to store it for 90 days\n          path: |\n            ${{ inputs.run_rpm && format('build-rpm/toucan-linux-x86_64-{0}.rpm', inputs.version) || '' }}\n            ${{ inputs.run_rpm && format('build-rpm/toucan-linux-{0}.zip', inputs.version) || '' }}\n            ${{ inputs.run_rpm && format('build-rpm/toucan-linux-{0}.sha256', inputs.version) || '' }}\n            ${{ inputs.run_deb && format('build-deb/toucan-linux-amd64-{0}.deb', inputs.version) || '' }}\n\n  test-and-upload:\n    runs-on: ubuntu-latest\n    needs: build-binaries\n    steps:\n      - name: Download artifacts\n        uses: actions/download-artifact@v4\n        with:\n          name: linux-artifacts\n          path: ./packages\n\n      - name: Check unpacked structure\n        run: find packages\n\n      - name: Test RPM in Fedora\n        if: inputs.run_rpm\n        run: |\n          docker run --rm -v \"$PWD/packages:/packages\" fedora \\\n            bash -c \"dnf install -y /packages/build-rpm/toucan-linux-x86_64-${{ inputs.version }}.rpm && toucan --version\"\n\n      - name: Upload RPM binary to tag\n        if: inputs.run_rpm\n        uses: AButler/upload-release-assets@v3.0\n        with:\n          files: packages/build-rpm/toucan-linux-x86_64-${{ inputs.version }}.rpm\n          repo-token: ${{ secrets.GITHUB_TOKEN }}\n          release-tag: ${{ github.ref_name }}\n\n      - name: Upload zipped Linux binaries\n        if: inputs.run_rpm\n        uses: AButler/upload-release-assets@v3.0\n        with:\n          files: packages/build-rpm/toucan-linux-${{ inputs.version }}.zip\n          repo-token: ${{ secrets.GITHUB_TOKEN }}\n          release-tag: ${{ github.ref_name }}\n\n      - name: Upload SHA256 for zipped Linux binaries\n        if: inputs.run_rpm\n        uses: AButler/upload-release-assets@v3.0\n        with:\n          files: packages/build-rpm/toucan-linux-${{ inputs.version }}.sha256\n          repo-token: ${{ secrets.GITHUB_TOKEN }}\n          release-tag: ${{ github.ref_name }}\n\n      - name: Test DEB in Ubuntu\n        if: inputs.run_deb\n        run: |\n          docker run --rm -v \"$PWD/packages:/packages\" ubuntu \\\n            bash -c '\n              apt-get update &&\n              apt-get install -y curl &&\n              dpkg -i /packages/build-deb/toucan-linux-amd64-${{ inputs.version }}.deb || apt-get install -f -y &&\n              toucan --version\n            '\n\n      - name: Upload DEB binary to tag\n        if: inputs.run_deb\n        uses: AButler/upload-release-assets@v3.0\n        with:\n          files: packages/build-deb/toucan-linux-amd64-${{ inputs.version }}.deb\n          repo-token: ${{ secrets.GITHUB_TOKEN }}\n          release-tag: ${{ github.ref_name }}"
  },
  {
    "path": ".github/workflows/macos.yml",
    "content": "name: Build and Publish macOS Binaries\non:\n  workflow_call:\n    inputs:\n      version:\n        required: true\n        type: string\n      run_pkg:\n        required: false\n        type: boolean\n        default: true\n      run_dmg:\n        required: false\n        type: boolean\n        default: true\n\njobs:\n\n  precheck:\n    runs-on: ubuntu-latest\n    outputs:\n      should_run: ${{ steps.check.outputs.should_run }}\n    steps:\n      - id: check\n        run: |\n          if [[ \"${{ inputs.run_pkg }}\" == \"true\" || \"${{ inputs.run_dmg }}\" == \"true\" ]]; then\n            echo \"✅ At least one packaging format enabled\"\n            echo \"should_run=true\" >> $GITHUB_OUTPUT\n          else\n            echo \"🚫 Both run_pkg and run_dmg are false — skipping workflow\"\n            echo \"should_run=false\" >> $GITHUB_OUTPUT\n          fi\n\n  build-binaries:\n    needs: precheck\n    if: needs.precheck.outputs.should_run == 'true'\n    runs-on: macos-14\n    permissions:\n      contents: write\n    steps:\n\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Import certificates\n        uses: apple-actions/import-codesign-certs@v3\n        with:\n          p12-file-base64: ${{ secrets.MAC_CERTIFICATES }}\n          p12-password: ${{ secrets.MAC_CERTIFICATES_PASSWORD }}\n\n      - name: Verify certificates\n        run: |\n          set -e\n\n          # Certs to check\n          certs=(\"Developer ID Application\" \"Developer ID Installer\")\n\n          # Expiration threshold in days (for warnings)\n          warning_days=30\n          warning_secs=$((warning_days * 86400))\n\n          for cert_name in \"${certs[@]}\"; do\n            # Try to get the certificate\n            if ! cert_pem=$(security find-certificate -c \"$cert_name\" -p); then\n              echo \"❌ Certificate '$cert_name' not found in keychain\"\n              exit 1\n            fi\n\n            not_after=$(echo \"$cert_pem\" | openssl x509 -noout -enddate | cut -d= -f2)\n            expiry_ts=$(date -j -f \"%b %e %T %Y %Z\" \"$not_after\" +%s 2>/dev/null || date -d \"$not_after\" +%s)\n            now_ts=$(date +%s)\n\n            if [ \"$expiry_ts\" -le \"$now_ts\" ]; then\n              echo \"❌ Certificate '$cert_name' is expired (expired on $not_after)\"\n              exit 1\n            fi\n\n            if [ $((expiry_ts - now_ts)) -le \"$warning_secs\" ]; then\n              echo \"⚠️ Certificate '$cert_name' expires soon on $not_after\"\n            fi\n          done\n\n      - name: Install Swift 6.3.1\n        run: |\n          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\n          sudo installer -pkg /tmp/swift.pkg -target /\n          TOOLCHAIN_PATH=\"/Library/Developer/Toolchains/swift-6.3.1-RELEASE.xctoolchain/usr/bin\"\n          echo \"$TOOLCHAIN_PATH\" >> $GITHUB_PATH\n          export PATH=\"$TOOLCHAIN_PATH:$PATH\"\n          swift --version\n\n      - name: Check for uncommitted changes\n        run: |\n          git_status=$(git status --porcelain)\n          \n          if [[ -n \"$git_status\" ]]; then\n            echo \"❌ Uncommitted changes detected:\"\n            echo \"$git_status\"\n            exit 1\n          else\n            echo \"✅ Working directory is clean. No uncommitted changes.\"\n          fi\n\n      - name: Build Swift binaries for arm64 and x86_64\n        if: inputs.run_pkg\n        run: |\n          chmod +x scripts/packaging/pkg.sh\n          swift build -c release --arch arm64\n          swift build -c release --arch x86_64\n\n      - name: Package .pkg and .zip\n        if: inputs.run_pkg\n        run: scripts/packaging/pkg.sh ${{ inputs.version }}\n        env:\n          MAC_APP_IDENTITY: ${{ secrets.MAC_APP_IDENTITY }}\n          MAC_INSTALLER_IDENTITY: ${{ secrets.MAC_INSTALLER_IDENTITY }}\n          APPLE_ID: ${{ secrets.APPLE_ID }}\n          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}\n          APP_SPECIFIC_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }}\n\n      - name: Verify .pkg file\n        if: inputs.run_pkg\n        run: |\n          PKG=\"release/toucan-macos-${{ inputs.version }}.pkg\"\n          echo \"🧪 Verifying $PKG\"\n          pkgutil --payload-files \"$PKG\"\n          echo \"✅ PKG passed verification\"\n\n      - name: Test installing .pkg file\n        if: inputs.run_pkg\n        run: |\n          PKG=\"release/toucan-macos-${{ inputs.version }}.pkg\"\n          echo \"📦 Installing $PKG to /\"\n          sudo installer -pkg \"$PKG\" -target /\n      \n          echo \"🔍 Checking for installed binaries\"\n          ls -lh /usr/local/bin/toucan*\n      \n          echo \"📈 Version output:\"\n          /usr/local/bin/toucan --version || echo \"⚠️ toucan binary failed to run\"\n\n      - name: Upload .pkg file to tag\n        if: inputs.run_pkg\n        uses: AButler/upload-release-assets@v3.0\n        with:\n          files: |\n            release/toucan-macos-${{ inputs.version }}.pkg\n          repo-token: ${{ secrets.GITHUB_TOKEN }}\n          release-tag: ${{ github.ref_name }}\n\n      - name: Upload .zip to tag\n        if: inputs.run_pkg\n        uses: AButler/upload-release-assets@v3.0\n        with:\n          files: release/toucan-macos-${{ inputs.version }}.zip\n          repo-token: ${{ secrets.GITHUB_TOKEN }}\n          release-tag: ${{ github.ref_name }}\n\n      - name: Upload SHA256 to tag\n        if: inputs.run_pkg\n        uses: AButler/upload-release-assets@v3.0\n        with:\n          files: release/toucan-macos-${{ inputs.version }}.sha256\n          repo-token: ${{ secrets.GITHUB_TOKEN }}\n          release-tag: ${{ github.ref_name }}\n\n      - name: Create .dmg file\n        if: inputs.run_pkg && inputs.run_dmg\n        run: ./scripts/packaging/dmg.sh ${{ inputs.version }}\n        env:\n          MAC_APP_IDENTITY: ${{ secrets.MAC_APP_IDENTITY }}\n          APPLE_ID: ${{ secrets.APPLE_ID }}\n          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}\n          APP_SPECIFIC_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }}\n\n      - name: Verify .dmg file structure and integrity\n        if: inputs.run_pkg && inputs.run_dmg\n        run: |\n          DMG=\"release/toucan-macos-${{ inputs.version }}.dmg\"\n          echo \"🧪 Verifying structure of $DMG\"\n          hdiutil verify \"$DMG\"\n          echo \"✅ Verified: $DMG is structurally valid\"\n\n      - name: Upload .dmg file to tag\n        if: inputs.run_pkg && inputs.run_dmg\n        uses: AButler/upload-release-assets@v3.0\n        with:\n          files: |\n            release/toucan-macos-${{ inputs.version }}.dmg\n          repo-token: ${{ secrets.GITHUB_TOKEN }}\n          release-tag: ${{ github.ref_name }}\n"
  },
  {
    "path": ".github/workflows/pr-for-formula.yml",
    "content": "name: Push Homebrew Formula\n\non:\n  workflow_call:\n    inputs:\n      version:\n        required: true\n        type: string\n      baseurl:\n        required: true\n        type: string\n      homepage:\n        required: true\n        type: string\n      formulafile:\n        required: true\n        type: string\n      formulaclass:\n        required: true\n        type: string\n      repository:\n        required: true\n        type: string\n    secrets:\n      HOMEBREW_TAP_PAT:\n        required: true\n\njobs:\n  push_formula:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout Tap Repo\n        uses: actions/checkout@v4\n        with:\n          repository: ${{ inputs.repository }} \n          token: ${{ secrets.HOMEBREW_TAP_PAT }}\n          ref: main\n\n      - name: Set Version Variables\n        run: |\n          echo \"VERSION=${{ inputs.version }}\" >> $GITHUB_ENV\n          echo \"BASE_URL=${{ inputs.baseurl }}\" >> $GITHUB_ENV\n          echo \"HOMEPAGE=${{ inputs.homepage }}\" >> $GITHUB_ENV\n\n      - name: Download SHA256 files from release\n        run: |\n          curl -LO \"$BASE_URL/toucan-linux-$VERSION.sha256\"\n          curl -LO \"$BASE_URL/toucan-macos-$VERSION.sha256\"\n\n      - name: Read SHA256 values\n        id: shas\n        run: |\n          macos_sha=$(awk '{print $1}' \"toucan-macos-$VERSION.sha256\")\n          linux_sha=$(awk '{print $1}' \"toucan-linux-$VERSION.sha256\")\n          echo \"macos_sha=$macos_sha\" >> $GITHUB_OUTPUT\n          echo \"linux_sha=$linux_sha\" >> $GITHUB_OUTPUT\n\n      - name: Write Formula File\n        run: |\n          mkdir -p Formula\n          cat > Formula/${{ inputs.formulafile }} <<EOF\n          class ${{ inputs.formulaclass }} < Formula\n            desc \"Toucan is a static site generator written in Swift.\"\n            homepage \"$HOMEPAGE\"\n            version \"$VERSION\"\n\n            if OS.mac?\n              url \"$BASE_URL/toucan-macos-$VERSION.zip\"\n              sha256 \"${{ steps.shas.outputs.macos_sha }}\"\n            elsif OS.linux?\n              url \"$BASE_URL/toucan-linux-$VERSION.zip\"\n              sha256 \"${{ steps.shas.outputs.linux_sha }}\"\n            end\n\n            def install\n              bin.install \"toucan\"\n              bin.install \"toucan-init\"\n              bin.install \"toucan-generate\"\n              bin.install \"toucan-serve\"\n              bin.install \"toucan-watch\"\n            end\n\n            test do\n              assert_match \"Usage\", shell_output(\"\\#{bin}/toucan --help\")\n            end\n          end\n          EOF\n\n      - name: Clean up SHA256 files\n        run: rm -f *.sha256\n\n      - name: Create Pull Request\n        uses: peter-evans/create-pull-request@v6\n        with:\n          token: ${{ secrets.HOMEBREW_TAP_PAT }}\n          commit-message: \"Update formula to version ${{ env.VERSION }}\"\n          branch: feature/update-formula-${{ env.VERSION }}\n          base: main\n          title: \"Update Homebrew formula to ${{ env.VERSION }}\"\n          body: |\n            This PR updates the Homebrew formula to version `${{ env.VERSION }}`."
  },
  {
    "path": ".github/workflows/tag_actions.yml",
    "content": "name: Dispatch macOS and Linux Builds on new tag\n\non:\n  push:\n    tags:\n      - 'v*'\n      - '[0-9]*'\n\npermissions:\n  contents: write\n\njobs:\n  prepare:\n    runs-on: ubuntu-latest\n    outputs:\n      version: ${{ steps.extract.outputs.version }}\n      original_version: ${{ steps.extract.outputs.original_version }}\n    steps:\n      - name: Extract version from tag\n        id: extract\n        run: |\n          RAW_VERSION=\"${GITHUB_REF#refs/tags/}\"\n          CLEAN_VERSION=\"${RAW_VERSION//-/.}\"\n          echo \"Version: $CLEAN_VERSION\"\n          echo \"VERSION=$CLEAN_VERSION\" >> $GITHUB_ENV\n          echo \"version=$CLEAN_VERSION\" >> $GITHUB_OUTPUT\n\n  linux:\n    needs: prepare\n    uses: ./.github/workflows/linux.yml\n    with:\n      version: ${{ needs.prepare.outputs.version }}\n      run_rpm: true\n      run_deb: true\n      static_stdlib: true\n    secrets: inherit\n\n  macos:\n    needs: prepare\n    uses: ./.github/workflows/macos.yml\n    with:\n      version: ${{ needs.prepare.outputs.version }}\n      run_pkg: true\n      run_dmg: false\n    secrets: inherit\n\n  pr_for_formula:\n    name: Create a PR for Homebrew Formula\n    needs: [prepare, linux, macos]\n    uses: ./.github/workflows/pr-for-formula.yml\n    with:\n      version: ${{ needs.prepare.outputs.version }}\n      baseurl: https://github.com/toucansites/toucan/releases/download/${{ github.ref_name }}\n      homepage: https://github.com/toucansites/toucan\n      formulafile: toucan.rb\n      formulaclass: Toucan\n      repository: toucansites/homebrew-toucan\n    secrets:\n      HOMEBREW_TAP_PAT: ${{ secrets.HOMEBREW_TAP_PAT }}"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\n.swiftpm\n.build\n.vscode\n.obsidian\n**/dist\n**/docs\nTests/sites/benchmark/\nExamples/try-o/\n"
  },
  {
    "path": ".swift-format",
    "content": "{\n  \"fileScopedDeclarationPrivacy\" : {\n    \"accessLevel\" : \"private\"\n  },\n  \"indentation\" : {\n    \"spaces\" : 4\n  },\n  \"multiElementCollectionTrailingCommas\": true,\n  \"indentConditionalCompilationBlocks\" : false,\n  \"indentSwitchCaseLabels\" : false,\n  \"lineBreakAroundMultilineExpressionChainComponents\" : true,\n  \"lineBreakBeforeControlFlowKeywords\" : true,\n  \"lineBreakBeforeEachArgument\" : true,\n  \"lineBreakBeforeEachGenericRequirement\" : true,\n  \"lineLength\" : 80,\n  \"maximumBlankLines\" : 1,\n  \"prioritizeKeepingFunctionOutputTogether\" : true,\n  \"respectsExistingLineBreaks\" : true,\n  \"rules\" : {\n    \"AllPublicDeclarationsHaveDocumentation\" : true,\n    \"AlwaysUseLowerCamelCase\" : false,\n    \"AmbiguousTrailingClosureOverload\" : true,\n    \"BeginDocumentationCommentWithOneLineSummary\" : false,\n    \"DoNotUseSemicolons\" : true,\n    \"DontRepeatTypeInStaticProperties\" : false,\n    \"FileScopedDeclarationPrivacy\" : true,\n    \"FullyIndirectEnum\" : true,\n    \"GroupNumericLiterals\" : true,\n    \"IdentifiersMustBeASCII\" : true,\n    \"NeverForceUnwrap\" : false,\n    \"NeverUseForceTry\" : false,\n    \"NeverUseImplicitlyUnwrappedOptionals\" : false,\n    \"NoAccessLevelOnExtensionDeclaration\" : false,\n    \"NoAssignmentInExpressions\" : true,\n    \"NoBlockComments\" : true,\n    \"NoCasesWithOnlyFallthrough\" : true,\n    \"NoEmptyTrailingClosureParentheses\" : true,\n    \"NoLabelsInCasePatterns\" : false,\n    \"NoLeadingUnderscores\" : false,\n    \"NoParensAroundConditions\" : true,\n    \"NoVoidReturnOnFunctionSignature\" : true,\n    \"OneCasePerLine\" : true,\n    \"OneVariableDeclarationPerLine\" : true,\n    \"OnlyOneTrailingClosureArgument\" : true,\n    \"OrderedImports\" : false,\n    \"ReturnVoidInsteadOfEmptyTuple\" : true,\n    \"UseEarlyExits\" : false,\n    \"UseLetInEveryBoundCaseVariable\" : false,\n    \"UseShorthandTypeNames\" : true,\n    \"UseSingleLinePropertyGetter\" : false,\n    \"UseSynthesizedInitializer\" : true,\n    \"UseTripleSlashForDocumentationComments\" : true,\n    \"UseWhereClausesInForLoops\" : false,\n    \"ValidateDocumentationComments\" : true\n  },\n  \"spacesAroundRangeFormationOperators\" : false,\n  \"tabWidth\" : 4,\n  \"version\" : 1\n}\n"
  },
  {
    "path": ".swiftformat",
    "content": "--swiftversion 6\n\n--indent 4\n    --indentstrings true\n    --smarttabs true\n    --xcodeindentation enabled\n\n--maxwidth 80\n--trimwhitespace always\n--self init-only\n--elseposition next-line\n--guardelse next-line\n--ranges nospace\n\n--wraparguments before-first\n--wrapparameters before-first\n--wrapcollections before-first\n--wrapconditions before-first\n\n\n--enable trailingclosures\n--enable todos\n--enable preferKeyPath\n\n--enable organizeDeclarations\n    --organizationmode type\n    --visibilityorder private, fileprivate, internal, package, public, open\n    --categorymark \"MARK: - %c\"\n    --markcategories false\n\n--enable wrapAttributes\n    --funcattributes prev-line\n    --typeattributes prev-line\n    --storedvarattrs prev-line\n    --computedvarattrs prev-line\n    --complexattrs prev-line\n\n--enable trailingCommas\n    --commas always\n\n--disable wrapSingleLineComments\n\n--enable yodaConditions\n\n--enable acronyms\n    --acronyms \"ID,URL,UUID,HTTP,YML,YAML,JSON,XML\"\n    --preserveacronyms \"rootUrl\"\n"
  },
  {
    "path": ".swiftformatignore",
    "content": "Package.swift"
  },
  {
    "path": ".swiftheaderignore",
    "content": ".*\n*.c\n*.h\n*.txt\n*.html\n*.yaml\nREADME.md\nPackage.resolved\nMakefile\nLICENSE\nPackage.swift\nDocker/**\nscripts/**\n"
  },
  {
    "path": "Docker/Dockerfile",
    "content": "FROM swift:6.3-noble AS build\n\nWORKDIR /build\nCOPY ./Package.* ./\nRUN swift package resolve\nCOPY Sources ./Sources\nCOPY Tests ./Tests\nCOPY Package.swift .\nCOPY Package.resolved .\nRUN swift build -c release --static-swift-stdlib\n\nWORKDIR /staging\nRUN cp \"$(swift build --package-path /build -c release --show-bin-path)/toucan\" ./\nRUN cp \"$(swift build --package-path /build -c release --show-bin-path)/toucan-generate\" ./\nRUN cp \"$(swift build --package-path /build -c release --show-bin-path)/toucan-init\" ./\nRUN cp \"$(swift build --package-path /build -c release --show-bin-path)/toucan-serve\" ./\nRUN cp \"$(swift build --package-path /build -c release --show-bin-path)/toucan-watch\" ./\nRUN cp \"/usr/libexec/swift/linux/swift-backtrace-static\" ./\nRUN find -L \"$(swift build --package-path /build -c release --show-bin-path)/\" -regex '.*\\.resources$' -exec cp -Ra {} ./ \\;\n\nFROM ubuntu:noble\n\nRUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \\\n    && apt-get -q update \\\n    && apt-get -q dist-upgrade -y \\\n    && apt-get -q install -y tzdata locales curl unzip \\\n    && ln -fs /usr/share/zoneinfo/UTC /etc/localtime \\\n    && dpkg-reconfigure -f noninteractive tzdata \\\n    && locale-gen en_US.UTF-8 \\\n    && update-locale LANG=en_US.UTF-8 \\\n    && rm -r /var/lib/apt/lists/*\n\nENV LANG=en_US.UTF-8\nENV LC_ALL=en_US.UTF-8\n\nRUN useradd --user-group --create-home --system --skel /dev/null --home-dir /app toucan\n\nWORKDIR /app\nCOPY --from=build --chown=toucan:toucan /staging /app\n\n# ✅ Ensure all files in /app are executable by all users\nRUN chmod -R a+rx /app\n\nENV SWIFT_BACKTRACE=enable=yes,sanitize=yes,threads=all,images=all,interactive=no,swift-backtrace=./swift-backtrace-static\nENV PATH=\"/app:$PATH\"\n\nUSER toucan:toucan\n\nEXPOSE 3000\n\nENTRYPOINT [\"/app/toucan\"]\nCMD [\"--help\"]\n"
  },
  {
    "path": "Docker/Dockerfile.testing",
    "content": "FROM swift:6.1\n\nWORKDIR /app\n\nCOPY . ./\n\nRUN swift package resolve\nRUN swift package clean\nRUN swift package update\n\nCMD [\"swift\", \"test\", \"--parallel\", \"--enable-code-coverage\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018-2022 Tibor Bödecs\nCopyright (c) 2022-2025 Binary Birds Ltd.\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this software and associated documentation\nfiles (the \"Software\"), to deal in the Software without\nrestriction, including without limitation the rights to use,\ncopy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\nOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\nHOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\nOTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "SHELL=/bin/bash\n\n.PHONY: docker\n\nbaseUrl = https://raw.githubusercontent.com/BinaryBirds/github-workflows/refs/heads/main/scripts\n\ncheck: symlinks language deps lint headers\n\nsymlinks:\n\tcurl -s $(baseUrl)/check-broken-symlinks.sh | bash\n\t\nlanguage:\n\tcurl -s $(baseUrl)/check-unacceptable-language.sh | bash\n\t\ndeps:\n\tcurl -s $(baseUrl)/check-local-swift-dependencies.sh | bash\n\t\nlint:\n\tcurl -s $(baseUrl)/run-swift-format.sh | bash\n\nfmt:\n\tswiftformat .\n\nformat:\n\tcurl -s $(baseUrl)/run-swift-format.sh | bash -s -- --fix\n\nheaders:\n\tcurl -s $(baseUrl)/check-swift-headers.sh | bash\n\nfix-headers:\n\tcurl -s $(baseUrl)/check-swift-headers.sh | bash -s -- --fix\n\nbuild:\n\tswift build\n\nrelease:\n\tswift build -c release\n\t\ntest:\n\tswift test --parallel\n\ntest-with-coverage:\n\tswift test --parallel --enable-code-coverage\n\nclean:\n\trm -rf .build\n\ninstall:\n\t./scripts/install-toucan.sh\n\nuninstall:\n\t./scripts/uninstall-toucan.sh\n\ndocker-image:\n\tdocker buildx build \\\n\t\t--platform linux/amd64,linux/arm64 \\\n\t\t-t toucan \\\n\t\t-f ./Docker/Dockerfile \\\n\t\t--load \\\n\t\t.\n\n# docker run --rm -v $(pwd):/app/site toucan generate /app/site/src /app/site/dist\n# docker run --rm -v $(pwd):/app/site toucan generate ./site/src ./site/dist --base-url \"http://localhost:3000\"\n# docker run --rm -v $(pwd):/app/site --entrypoint /app/toucan toucan generate ./site/src ./site/dist --base-url \"http://localhost:3000\"\n# docker run --rm -p 3000:3000 -v $(pwd):/app/site toucan serve --hostname \"0.0.0.0\" --port 3000 ./site/dist\n# docker run --rm -v $(pwd):/app/site --entrypoint toucan toucansites/toucan generate /app/site/src /app/site/dist\n\ndocker-run:\n\tdocker run --rm -v $(pwd):/app -it swift:6.1\n\ndocker-tests:\n\tdocker build -t toucan-tests . -f ./Docker/Dockerfile.testing && docker run --rm toucan-tests\n\ndiff:\n\tdiff --color=always -r dist-live dist --exclude=api || true\n"
  },
  {
    "path": "Package.resolved",
    "content": "{\n  \"originHash\" : \"b0aa8e8208e365a7988670c760c7c1a87737dac7471c3bcff7b3381dbaabf37b\",\n  \"pins\" : [\n    {\n      \"identity\" : \"async-http-client\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/swift-server/async-http-client.git\",\n      \"state\" : {\n        \"revision\" : \"5dd84c7bb48b348751d7bbe7ba94a17bafdcef37\",\n        \"version\" : \"1.30.2\"\n      }\n    },\n    {\n      \"identity\" : \"file-manager-kit\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/binarybirds/file-manager-kit\",\n      \"state\" : {\n        \"revision\" : \"89a7485d5564aafd77d846fe4366f7e67d6b4b9b\",\n        \"version\" : \"0.4.0\"\n      }\n    },\n    {\n      \"identity\" : \"filemonitor\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/toucansites/FileMonitor\",\n      \"state\" : {\n        \"revision\" : \"082b744b35a2f3d53e19bc1925d358590761551f\",\n        \"version\" : \"0.1.0\"\n      }\n    },\n    {\n      \"identity\" : \"hummingbird\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/hummingbird-project/hummingbird\",\n      \"state\" : {\n        \"revision\" : \"3ae359b1bb1e72378ed43b59fdcd4d44cac5d7a4\",\n        \"version\" : \"2.16.0\"\n      }\n    },\n    {\n      \"identity\" : \"semver.swift\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/johnfairh/Semver.swift.git\",\n      \"state\" : {\n        \"revision\" : \"0a6e2fe061ecb840c9bf80b6427e70ac039239fa\",\n        \"version\" : \"1.2.4\"\n      }\n    },\n    {\n      \"identity\" : \"sourcemapper\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/johnfairh/SourceMapper.git\",\n      \"state\" : {\n        \"revision\" : \"2c86d8f44beb2c41effa2f9c5f6cf29b871bf3b9\",\n        \"version\" : \"2.0.0\"\n      }\n    },\n    {\n      \"identity\" : \"swift-algorithms\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-algorithms\",\n      \"state\" : {\n        \"revision\" : \"87e50f483c54e6efd60e885f7f5aa946cee68023\",\n        \"version\" : \"1.2.1\"\n      }\n    },\n    {\n      \"identity\" : \"swift-argument-parser\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-argument-parser\",\n      \"state\" : {\n        \"revision\" : \"c5d11a805e765f52ba34ec7284bd4fcd6ba68615\",\n        \"version\" : \"1.7.0\"\n      }\n    },\n    {\n      \"identity\" : \"swift-asn1\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-asn1.git\",\n      \"state\" : {\n        \"revision\" : \"eb50cbd14606a9161cbc5d452f18797c90ef0bab\",\n        \"version\" : \"1.7.0\"\n      }\n    },\n    {\n      \"identity\" : \"swift-async-algorithms\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-async-algorithms\",\n      \"state\" : {\n        \"revision\" : \"9d349bcc328ac3c31ce40e746b5882742a0d1272\",\n        \"version\" : \"1.1.3\"\n      }\n    },\n    {\n      \"identity\" : \"swift-atomics\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-atomics.git\",\n      \"state\" : {\n        \"revision\" : \"b601256eab081c0f92f059e12818ac1d4f178ff7\",\n        \"version\" : \"1.3.0\"\n      }\n    },\n    {\n      \"identity\" : \"swift-certificates\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-certificates.git\",\n      \"state\" : {\n        \"revision\" : \"bde8ca32a096825dfce37467137c903418c1893d\",\n        \"version\" : \"1.19.1\"\n      }\n    },\n    {\n      \"identity\" : \"swift-cmark\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/swiftlang/swift-cmark.git\",\n      \"state\" : {\n        \"revision\" : \"5d9bdaa4228b381639fff09403e39a04926e2dbe\",\n        \"version\" : \"0.7.1\"\n      }\n    },\n    {\n      \"identity\" : \"swift-collections\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-collections.git\",\n      \"state\" : {\n        \"revision\" : \"6675bc0ff86e61436e615df6fc5174e043e57924\",\n        \"version\" : \"1.4.1\"\n      }\n    },\n    {\n      \"identity\" : \"swift-crypto\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-crypto.git\",\n      \"state\" : {\n        \"revision\" : \"1b6b2e274e85105bfa155183145a1dcfd63331f1\",\n        \"version\" : \"4.5.0\"\n      }\n    },\n    {\n      \"identity\" : \"swift-css-parser\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/stackotter/swift-css-parser\",\n      \"state\" : {\n        \"revision\" : \"6cf16c6696def00a313daef0e29eb27f5c39ece4\",\n        \"version\" : \"0.1.2\"\n      }\n    },\n    {\n      \"identity\" : \"swift-distributed-tracing\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-distributed-tracing.git\",\n      \"state\" : {\n        \"revision\" : \"dc4030184203ffafbb2ec614352487235d747fe0\",\n        \"version\" : \"1.4.1\"\n      }\n    },\n    {\n      \"identity\" : \"swift-http-structured-headers\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-http-structured-headers.git\",\n      \"state\" : {\n        \"revision\" : \"933538faa42c432d385f02e07df0ace7c5ecfc47\",\n        \"version\" : \"1.7.0\"\n      }\n    },\n    {\n      \"identity\" : \"swift-http-types\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-http-types.git\",\n      \"state\" : {\n        \"revision\" : \"45eb0224913ea070ec4fba17291b9e7ecf4749ca\",\n        \"version\" : \"1.5.1\"\n      }\n    },\n    {\n      \"identity\" : \"swift-log\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-log\",\n      \"state\" : {\n        \"revision\" : \"2778fd4e5a12a8aaa30a3ee8285f4ce54c5f3181\",\n        \"version\" : \"1.9.1\"\n      }\n    },\n    {\n      \"identity\" : \"swift-markdown\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-markdown\",\n      \"state\" : {\n        \"revision\" : \"7d9a5ce307528578dfa777d505496bd5f544ad94\",\n        \"version\" : \"0.7.3\"\n      }\n    },\n    {\n      \"identity\" : \"swift-metrics\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-metrics.git\",\n      \"state\" : {\n        \"revision\" : \"d51c8d13fa366eec807eedb4e37daa60ff5bfdd5\",\n        \"version\" : \"2.10.1\"\n      }\n    },\n    {\n      \"identity\" : \"swift-mustache\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/hummingbird-project/swift-mustache\",\n      \"state\" : {\n        \"revision\" : \"2e2a84698dd8a5fff2fe28857f0f95bb03d21d64\",\n        \"version\" : \"2.0.2\"\n      }\n    },\n    {\n      \"identity\" : \"swift-nio\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-nio.git\",\n      \"state\" : {\n        \"revision\" : \"bdf004b44f77c56fca752cd1cf243c802f8469c9\",\n        \"version\" : \"2.97.0\"\n      }\n    },\n    {\n      \"identity\" : \"swift-nio-extras\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-nio-extras.git\",\n      \"state\" : {\n        \"revision\" : \"5a48717e29f62cb8326d6d42e46b562ca93847a6\",\n        \"version\" : \"1.34.0\"\n      }\n    },\n    {\n      \"identity\" : \"swift-nio-http2\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-nio-http2.git\",\n      \"state\" : {\n        \"revision\" : \"81cc18264f92cd307ff98430f89372711d4f6fe9\",\n        \"version\" : \"1.43.0\"\n      }\n    },\n    {\n      \"identity\" : \"swift-nio-ssl\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-nio-ssl.git\",\n      \"state\" : {\n        \"revision\" : \"3f337058ccd7243c4cac7911477d8ad4c598d4da\",\n        \"version\" : \"2.37.0\"\n      }\n    },\n    {\n      \"identity\" : \"swift-nio-transport-services\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-nio-transport-services.git\",\n      \"state\" : {\n        \"revision\" : \"67787bb645a5e67d2edcdfbe48a216cc549222d5\",\n        \"version\" : \"1.28.0\"\n      }\n    },\n    {\n      \"identity\" : \"swift-numerics\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-numerics.git\",\n      \"state\" : {\n        \"revision\" : \"0c0290ff6b24942dadb83a929ffaaa1481df04a2\",\n        \"version\" : \"1.1.1\"\n      }\n    },\n    {\n      \"identity\" : \"swift-protobuf\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-protobuf.git\",\n      \"state\" : {\n        \"revision\" : \"81558271e243f8f47dfe8e9fdd55f3c2b5413f68\",\n        \"version\" : \"1.37.0\"\n      }\n    },\n    {\n      \"identity\" : \"swift-sass\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/johnfairh/swift-sass\",\n      \"state\" : {\n        \"revision\" : \"361b70bbb4f038cc850cb2d5e6ef5aa3495cb6ef\",\n        \"version\" : \"3.3.0\"\n      }\n    },\n    {\n      \"identity\" : \"swift-service-context\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-service-context.git\",\n      \"state\" : {\n        \"revision\" : \"d0997351b0c7779017f88e7a93bc30a1878d7f29\",\n        \"version\" : \"1.3.0\"\n      }\n    },\n    {\n      \"identity\" : \"swift-service-lifecycle\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/swift-server/swift-service-lifecycle.git\",\n      \"state\" : {\n        \"revision\" : \"9829955b385e5bb88128b73f1b8389e9b9c3191a\",\n        \"version\" : \"2.11.0\"\n      }\n    },\n    {\n      \"identity\" : \"swift-system\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-system\",\n      \"state\" : {\n        \"revision\" : \"7c6ad0fc39d0763e0b699210e4124afd5041c5df\",\n        \"version\" : \"1.6.4\"\n      }\n    },\n    {\n      \"identity\" : \"swiftcommand\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/Zollerboy1/SwiftCommand\",\n      \"state\" : {\n        \"revision\" : \"28efb038351a8c45010772b407adb9ea02ba67b5\",\n        \"version\" : \"1.4.2\"\n      }\n    },\n    {\n      \"identity\" : \"swiftsoup\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/scinfu/SwiftSoup\",\n      \"state\" : {\n        \"revision\" : \"6c7915e16f729857aec3e99068c361e58a00ed68\",\n        \"version\" : \"2.13.4\"\n      }\n    },\n    {\n      \"identity\" : \"version\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/mxcl/Version\",\n      \"state\" : {\n        \"revision\" : \"3043fcd2a50375db76d89ff206a612471833d1c2\",\n        \"version\" : \"2.2.1\"\n      }\n    },\n    {\n      \"identity\" : \"yams\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/jpsim/Yams\",\n      \"state\" : {\n        \"revision\" : \"3d6871d5b4a5cd519adf233fbb576e0a2af71c17\",\n        \"version\" : \"5.4.0\"\n      }\n    }\n  ],\n  \"version\" : 3\n}\n"
  },
  {
    "path": "Package.swift",
    "content": "// swift-tools-version: 6.1\nimport PackageDescription\n\nlet swiftSettings: [SwiftSetting] = [\n    .enableExperimentalFeature(\"StrictConcurrency=complete\"),\n]\n\n/// Git commit hash information based on context.\nvar gitCommitHash: String {\n    if let git = Context.gitInformation {\n        let base = git.currentCommit\n        return git.hasUncommittedChanges ? \"\\(base)-dev\" : base\n    }\n    return \"untracked\"\n}\n\nlet package = Package(\n    name: \"toucan\",\n    platforms: [\n        .macOS(.v14),\n        .iOS(.v17),\n        .tvOS(.v17),\n        .watchOS(.v10),\n        .visionOS(.v1),\n    ],\n    products: [\n        .executable(name: \"toucan\", targets: [\"toucan\"]),\n        .executable(name: \"toucan-init\", targets: [\"toucan-init\"]),\n        .executable(name: \"toucan-generate\", targets: [\"toucan-generate\"]),\n        .executable(name: \"toucan-watch\", targets: [\"toucan-watch\"]),\n        .executable(name: \"toucan-serve\", targets: [\"toucan-serve\"]),\n\n        .library(name: \"ToucanCore\", targets: [\"ToucanCore\"]),\n        .library(name: \"ToucanSerialization\", targets: [\"ToucanSerialization\"]),\n        .library(name: \"ToucanMarkdown\", targets: [\"ToucanMarkdown\"]),\n        .library(name: \"ToucanSource\", targets: [\"ToucanSource\"]),\n        .library(name: \"ToucanSDK\", targets: [\"ToucanSDK\"]),\n    ],\n    dependencies: [\n        .package(\n            url: \"https://github.com/apple/swift-argument-parser\",\n            \"1.5.0\"..<\"1.7.1\" // breaks dep graph for 6.0\n        ),\n        .package(\n            url: \"https://github.com/apple/swift-markdown\",\n            from: \"0.6.0\"\n        ),\n        .package(\n            url: \"https://github.com/apple/swift-log\",\n            \"1.6.0\"..<\"1.10.0\"\n        ),\n        .package(\n            url: \"https://github.com/apple/swift-nio\",\n            \"2.0.0\"..<\"2.97.1\" // breaks dep graph for 6.0\n        ),\n        .package(\n            url: \"https://github.com/binarybirds/file-manager-kit\",\n            exact: \"0.4.0\"\n        ),\n        .package(\n            url: \"https://github.com/swift-server/async-http-client\",\n            \"1.0.0\"..<\"1.30.3\" // swift configuration breaks on 6.3.1\n        ),\n        .package(\n            url: \"https://github.com/hummingbird-project/hummingbird\",\n            \"2.0.0\"..<\"2.17.0\" // swift configuration breaks on 6.3.1\n        ),\n        .package(\n            url: \"https://github.com/hummingbird-project/swift-mustache\",\n            from: \"2.0.0\"\n        ),\n        .package(\n            url: \"https://github.com/jpsim/Yams\",\n            from: \"5.4.0\"\n        ),\n        .package(\n            url: \"https://github.com/scinfu/SwiftSoup\",\n            from: \"2.8.0\"\n        ),\n        .package(\n            url: \"https://github.com/toucansites/FileMonitor\",\n            from: \"0.1.0\"\n        ),\n        .package(\n            url: \"https://github.com/Zollerboy1/SwiftCommand\",\n            from: \"1.4.0\"\n        ),\n        .package(\n            url: \"https://github.com/johnfairh/swift-sass\",\n            from: \"3.1.0\"\n        ),\n        .package(\n            url: \"https://github.com/stackotter/swift-css-parser\",\n            from: \"0.1.2\"\n        ),\n        .package(\n            url: \"https://github.com/mxcl/Version\",\n            from: \"2.2.0\"\n        ),\n    ],\n    targets: [\n        .target(\n            name: \"_GitCommitHash\",\n            cSettings: [\n                .define(\"GIT_COMMIT_HASH\", to: #\"\"\\#(gitCommitHash)\"\"#)\n            ]\n        ),\n        // MARK: - executable targets\n\n        .executableTarget(\n            name: \"toucan\",\n            dependencies: [\n                .product(\n                    name: \"ArgumentParser\",\n                    package: \"swift-argument-parser\"\n                ),\n                .product(name: \"Logging\", package: \"swift-log\"),\n                .product(name: \"SwiftCommand\", package: \"SwiftCommand\"),\n                .target(name: \"ToucanCore\"),\n            ],\n            swiftSettings: swiftSettings\n        ),\n        .executableTarget(\n            name: \"toucan-init\",\n            dependencies: [\n                .product(\n                    name: \"ArgumentParser\",\n                    package: \"swift-argument-parser\"\n                ),\n                .product(name: \"Logging\", package: \"swift-log\"),\n                .product(name: \"FileManagerKit\", package: \"file-manager-kit\"),\n                .product(name: \"SwiftCommand\", package: \"SwiftCommand\"),\n                .target(name: \"ToucanCore\"),\n                .target(name: \"ToucanSource\")\n            ],\n            swiftSettings: swiftSettings\n        ),\n        .executableTarget(\n            name: \"toucan-generate\",\n            dependencies: [\n                .product(\n                    name: \"ArgumentParser\",\n                    package: \"swift-argument-parser\"\n                ),\n                .product(name: \"Logging\", package: \"swift-log\"),\n                .target(name: \"ToucanSDK\"),\n            ],\n            swiftSettings: swiftSettings\n        ),\n        .executableTarget(\n            name: \"toucan-watch\",\n            dependencies: [\n                .product(\n                    name: \"ArgumentParser\",\n                    package: \"swift-argument-parser\"\n                ),\n                .product(name: \"Logging\", package: \"swift-log\"),\n                .product(name: \"FileMonitor\", package: \"FileMonitor\"),\n                .product(name: \"SwiftCommand\", package: \"SwiftCommand\"),\n                .target(name: \"ToucanCore\"),\n            ],\n            swiftSettings: swiftSettings\n        ),\n        .executableTarget(\n            name: \"toucan-serve\",\n            dependencies: [\n                .product(\n                    name: \"ArgumentParser\",\n                    package: \"swift-argument-parser\"\n                ),\n                .product(name: \"Logging\", package: \"swift-log\"),\n                .product(name: \"Hummingbird\", package: \"hummingbird\"),\n                .target(name: \"ToucanCore\"),\n            ],\n            swiftSettings: swiftSettings\n        ),\n\n        // MARK: - regular targets\n\n        .target(\n            name: \"ToucanCore\",\n            dependencies: [\n                .product(name: \"Logging\", package: \"swift-log\"),\n                .product(name: \"FileManagerKit\", package: \"file-manager-kit\"),\n                .product(name: \"Version\", package: \"Version\"),\n                .target(name: \"_GitCommitHash\"),\n            ],\n            swiftSettings: swiftSettings\n        ),\n        .target(\n            name: \"ToucanSerialization\",\n            dependencies: [\n                .product(name: \"Yams\", package: \"yams\"),\n                .target(name: \"ToucanCore\"),\n            ],\n            swiftSettings: swiftSettings\n        ),\n        .target(\n            name: \"ToucanMarkdown\",\n            dependencies: [\n                // for outline\n                .product(name: \"SwiftSoup\", package: \"SwiftSoup\"),\n                // for markdown to html\n                .product(name: \"Markdown\", package: \"swift-markdown\"),\n                // for transformers\n                .product(name: \"SwiftCommand\", package: \"SwiftCommand\"),\n                .target(name: \"ToucanCore\"),\n            ],\n            swiftSettings: swiftSettings\n        ),\n        .target(\n            name: \"ToucanSource\",\n            dependencies: [\n                .target(name: \"ToucanCore\"),\n                .target(name: \"ToucanSerialization\"),\n            ],\n            swiftSettings: swiftSettings\n        ),\n        .target(\n            name: \"ToucanSDK\",\n            dependencies: [\n                .product(name: \"Mustache\", package: \"swift-mustache\"),\n                .product(name: \"DartSass\", package: \"swift-sass\"),\n                .product(name: \"SwiftCSSParser\", package: \"swift-css-parser\"),\n                .target(name: \"ToucanCore\"),\n                .target(name: \"ToucanSerialization\"),\n                .target(name: \"ToucanMarkdown\"),\n                .target(name: \"ToucanSource\"),\n            ],\n            swiftSettings: swiftSettings\n        ),\n\n        // MARK: - test targets\n\n        .testTarget(\n            name: \"ToucanCoreTests\",\n            dependencies: [\n                .target(name: \"ToucanCore\"),\n            ]\n        ),\n        .testTarget(\n            name: \"ToucanMarkdownTests\",\n            dependencies: [\n                .target(name: \"ToucanMarkdown\"),\n            ]\n        ),\n        .testTarget(\n            name: \"ToucanSourceTests\",\n            dependencies: [\n                .target(name: \"ToucanCore\"),\n                .target(name: \"ToucanSource\"),\n                .product(\n                    name: \"FileManagerKitBuilder\",\n                    package: \"file-manager-kit\"\n                ),\n            ]\n        ),\n        .testTarget(\n            name: \"ToucanSDKTests\",\n            dependencies: [\n                .target(name: \"ToucanSDK\"),\n                .product(\n                    name: \"FileManagerKitBuilder\",\n                    package: \"file-manager-kit\"\n                ),\n            ]\n        ),\n    ]\n)\n"
  },
  {
    "path": "README.md",
    "content": "# Toucan\n\nToucan is a markdown-based Static Site Generator (SSG) written in Swift.\n\n## Installation\n\n## Compile from source\n\nMake sure you have Swift 6+ installed. See [how to install swift](https://www.swift.org/install/) for instructions.\n\nTo build Toucan from source, run the following commands:\n\n```shell\n# clone the Toucan repository\ngit clone https://github.com/toucansites/toucan.git\ncd toucan\n\n# install Toucan on your system under /usr/local/bin\nmake install\n# enter your password, if needed\n\n# verify\nwhich toucan\n# should return /usr/local/bin/toucan\n\n# uninstall, remove Toucan from your system\nmake uninstall\n# enter your password, if needed\n```\n\n## Quickstart\n\nTo quickly bootstrap a Toucan-based static site, run the following commands:\n\n```shell\ntoucan init my-site\ncd my-site\ntoucan generate\ntoucan serve\n# Visit: http://localhost:3000\n```\n\n## Documentation\n\nThe complete documentation for Toucan is available on [toucansites.com](https://toucansites.com/docs/).\n"
  },
  {
    "path": "Sources/ToucanCore/Extensions/Dictionary+Extensions.swift",
    "content": "//\n//  Dictionary+Extensions.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 02. 03..\n//\n\npublic extension Dictionary {\n    /// Transforms the keys of the dictionary using the given closure, preserving the associated values.\n    ///\n    /// This method applies the provided transformation to each key in the dictionary,\n    /// resulting in a new dictionary with the transformed keys and original values.\n    ///\n    /// - Parameter t: A closure that takes a key as input and returns a transformed key.\n    /// - Returns: A dictionary with transformed keys and the original values.\n    func mapKeys<T>(\n        _ t: (Key) throws -> T\n    ) rethrows -> [T: Value] {\n        try .init(\n            uniqueKeysWithValues: map { try (t($0.key), $0.value) }\n        )\n    }\n}\n\n/// This extension allows recursive merging of dictionaries with String keys and Any values.\npublic extension Dictionary where Key == String {\n    /// Recursively merges another `[String: Value]` dictionary into the current dictionary and returns a new dictionary.\n    ///\n    /// - Parameter other: The dictionary to merge into the current dictionary.\n    /// - Returns: A new dictionary with the merged contents.\n    func recursivelyMerged(\n        with other: [String: Value]\n    ) -> [String: Value] {\n        var result = self\n        for (key, value) in other {\n            if let existingValue = result[key] as? [String: Value],\n                let newValue = value as? [String: Value]\n            {\n                result[key] =\n                    existingValue.recursivelyMerged(with: newValue) as? Value\n            }\n            else {\n                result[key] = value\n            }\n        }\n        return result\n    }\n\n    /// Retrieves a nested value from the receiver using a dot-separated key path.\n    /// Supports traversal through dictionaries with `String` keys and arrays with numeric indices.\n    ///\n    /// - Parameter keyPath: A dot-separated string representing the path to the nested value.\n    /// - Returns: The value at the specified key path, or `nil` if the path is invalid.\n    func value(\n        forKeyPath keyPath: String\n    ) -> Any? {\n        let keys = keyPath.split(separator: \".\").map(String.init)\n        var current: Any? = self\n\n        for key in keys {\n            if let dict = current as? [String: Any], let next = dict[key] {\n                current = next\n                continue\n            }\n\n            if let array = current as? [Any], let index = Int(key),\n                array.indices.contains(index)\n            {\n                current = array[index]\n                continue\n            }\n\n            return nil\n        }\n\n        return current\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanCore/Extensions/Logging+Extensions.swift",
    "content": "//\n//  Logging+Extensions.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 18..\n//\n\nimport class Foundation.ProcessInfo\nimport Logging\n\npublic extension Logger {\n\n    /// Returns a logger instance for the specified subsystem.\n    ///\n    /// Constructs a logger with a label based on the subsystem identifier and sets its log level.\n    /// The log level is determined by checking environment variables\n    /// for subsystem-specific or global log level settings. If none are found, the provided default level is used.\n    ///\n    /// - Parameters:\n    ///   - id: The subsystem identifier (e.g., `\"generate\"`, `\"object-loader\"`).\n    ///   - level: The default log level to use if not specified elsewhere. Defaults to `.info`.\n    /// - Returns: A configured `Logger` instance for the subsystem.\n    static func subsystem(\n        _ id: String = \"\",\n        _ level: Logger.Level = .info\n    ) -> Logger {\n        var logger = Logger(label: id.loggerLabel())\n        logger.logLevel = findEnvLogLevel(id) ?? level\n\n        return logger\n    }\n}\n\nprivate extension Logger {\n\n    /// Returns the log level from environment variables for the given subsystem identifier.\n    ///\n    /// Checks for a subsystem-specific log level key and a global log level key (`TOUCAN_LOG_LEVEL`)\n    /// in the environment. If a valid log level string is found, it is converted to a `Logger.Level`.\n    ///\n    /// - Parameter id: The subsystem identifier.\n    /// - Returns: The log level if found and valid, otherwise `nil`.\n    static func findEnvLogLevel(_ id: String) -> Logger.Level? {\n        let env = ProcessInfo.processInfo.environment\n        let keys = [\n            id.subsystemLogLevelKey(),\n            \"TOUCAN_LOG_LEVEL\",\n        ]\n\n        for key in keys {\n            if let rawLevel = env[key]?.lowercased(),\n                let level = Logger.Level(rawValue: rawLevel)\n            {\n                return level\n            }\n        }\n\n        return nil\n    }\n}\n\nprivate extension String {\n\n    /// Returns the logger label for a subsystem.\n    ///\n    /// Constructs a logger label by joining \"TOUCAN\" and the subsystem identifier with a hyphen.\n    /// If the identifier is empty, returns \"toucan\".\n    ///\n    /// - Examples:\n    ///   - For an empty string: `\"toucan\"`\n    ///   - For `\"generate\"`: `\"toucan-generate\"`\n    ///   - For `\"object-loader\"`: `\"toucan-object-loader\"`\n    func loggerLabel() -> String {\n        let prefix = \"toucan\"\n        let parts = isEmpty ? [prefix] : [prefix, self]\n        return parts.joined(separator: \"-\")\n    }\n\n    /// Returns the environment variable key for the log level of a subsystem.\n    ///\n    /// This method constructs a log level key by converting the logger label to uppercase,\n    /// replacing hyphens with underscores, and appending \"_LOG_LEVEL\".\n    ///\n    /// - Examples:\n    ///   - For an empty string: `\"TOUCAN_LOG_LEVEL\"`\n    ///   - For `\"generate\"`: `\"TOUCAN_GENERATE_LOG_LEVEL\"`\n    ///   - For `\"object-loader\"`: `\"TOUCAN_OBJECT_LOADER_LOG_LEVEL\"`\n    func subsystemLogLevelKey() -> String {\n        loggerLabel()\n            .uppercased()\n            .replacing(\"-\", with: \"_\")\n            .appending(\"_LOG_LEVEL\")\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanCore/Extensions/String+Extensions.swift",
    "content": "//\n//  String+Extensions.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 17..\n//\n\nimport Foundation\n\npublic extension String {\n    /// A convenience property that converts an empty string to `nil`.\n    ///\n    /// This is useful for cases where an empty string should be treated as the absence of a value,\n    /// such as when preparing optional fields for encoding, form validation, or API payloads.\n    ///\n    /// For example:\n    /// ```swift\n    /// let name: String = \"\"\n    /// let optionalName = name.emptyToNil // Result: nil\n    /// ```\n    ///\n    /// - Returns: `nil` if the string is empty; otherwise, returns the original string.\n    var emptyToNil: String? {\n        isEmpty ? nil : self\n    }\n\n    /// Removes the leading slash from the string if present.\n    ///\n    /// This method checks if the string starts with a slash (`/`). If so, it removes it.\n    ///\n    /// - Returns: A new string without a leading slash, or the original string if no leading slash exists.\n    func dropLeadingSlash() -> String {\n        if hasPrefix(\"/\") {\n            return String(dropFirst())\n        }\n        return self\n    }\n\n    /// Removes the trailing slash from the string if present.\n    ///\n    /// This method checks if the string ends with a slash (`/`). If so, it removes it.\n    ///\n    /// - Returns: A new string without a trailing slash, or the original string if no trailing slash exists.\n    func dropTrailingSlash() -> String {\n        if hasSuffix(\"/\") {\n            return String(dropLast())\n        }\n        return self\n    }\n\n    /// Ensures the string starts with a leading slash.\n    ///\n    /// This method checks if the string already begins with a slash (`/`). If it does, the original string is returned.\n    /// Otherwise, it prepends a slash to the beginning of the string.\n    ///\n    /// - Returns: A new string with a leading slash ensured.\n    func ensureLeadingSlash() -> String {\n        if hasPrefix(\"/\") {\n            return self\n        }\n        return \"/\" + self\n    }\n\n    /// Appends a trailing slash to the string if not already present.\n    ///\n    /// This method checks if the string ends with a slash (`/`). If not, it appends one.\n    ///\n    /// - Returns: A new string with a trailing slash ensured.\n    func ensureTrailingSlash() -> String {\n        if hasSuffix(\"/\") {\n            return self\n        }\n        return self + \"/\"\n    }\n\n    /// Replaces substrings in the string using a given dictionary of replacements.\n    ///\n    /// This method iterates over the key-value pairs in the provided dictionary\n    /// and replaces all occurrences of each key with its corresponding value.\n    ///\n    /// - Parameter dictionary: A dictionary where each key is a substring to search for,\n    ///   and the corresponding value is the string to replace it with.\n    /// - Returns: A new string with all specified substrings replaced.\n    func replacing(\n        _ dictionary: [String: String]\n    ) -> String {\n        var result = self\n        for (key, value) in dictionary {\n            result = result.replacing(key, with: value)\n        }\n        return result\n    }\n\n    /// Converts the string into a URL-friendly slug.\n    ///\n    /// This method removes diacritics, trims whitespace, lowercases the string,\n    /// and keeps only alphanumeric characters, dashes, underscores, and periods.\n    /// Invalid characters are removed, and remaining components are joined with hyphens.\n    ///\n    /// - Returns: A slugified version of the original string.\n    func slugify() -> String {\n        let allowed = CharacterSet(\n            charactersIn: \"abcdefghijklmnopqrstuvwxyz0123456789-_.\"\n        )\n        return trimmingCharacters(in: .whitespacesAndNewlines)\n            .lowercased()\n            .folding(\n                options: .diacriticInsensitive,\n                locale: .init(identifier: \"en-US\")\n            )\n            .components(separatedBy: allowed.inverted)\n            .filter { $0 != \"\" }\n            .joined(separator: \"-\")\n    }\n\n    /// Resolves a relative asset path by combining it with a base URL, assets path, and slug.\n    ///\n    /// This method builds a complete asset URL by handling various cases:\n    /// - If the base URL or assets path is empty, it returns the original string.\n    /// - If the string starts with `/`, it appends the string directly to the base URL.\n    /// - If the string starts with a relative prefix (e.g., `./assetsPath/`), it removes the prefix\n    ///   and combines the base URL, assets path, slug, and remaining path parts into a full URL.\n    ///\n    /// - Parameters:\n    ///   - baseURL: The base URL used to form the full path.\n    ///   - assetsPath: The relative directory for the assets.\n    ///   - slug: A string inserted in the final path for identification or grouping.\n    /// - Returns: A full string URL combining all parts, or the original string if no resolution is applied.\n    func resolveAsset(\n        baseURL: String,\n        assetsPath: String,\n        slug: String\n    ) -> String {\n        if baseURL.isEmpty || assetsPath.isEmpty {\n            return self\n        }\n\n        let baseURL = baseURL.dropTrailingSlash()\n        if hasPrefix(\"/\") {\n            return [baseURL, dropLeadingSlash()].joined(separator: \"/\")\n        }\n\n        let prefix = \"./\\(assetsPath)/\"\n        guard hasPrefix(prefix) else {\n            return self\n        }\n\n        let src = String(dropFirst(prefix.count))\n\n        return [baseURL, assetsPath, slug, src]\n            .filter { !$0.isEmpty }\n            .joined(separator: \"/\")\n    }\n\n    /// Checks if a string contains only valid URL characters.\n    ///\n    /// Allowed: unreserved (`A–Z a–z 0–9 - . _ ~`), reserved\n    /// (`:/?#[]@!$&'()*+,;=`), and `%` for encoding.\n    ///\n    /// - Returns: `true` if all characters are valid, otherwise `false`.\n    func containsOnlyValidURLCharacters() -> Bool {\n        let alphabet = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\"\n        let numerics = \"0123456789\"\n        let special = \"-._~{}%\"\n        let reserved = \":/?#[]@!$&'()*+,;=\"\n\n        let allowed = CharacterSet(\n            charactersIn: alphabet + numerics + special + reserved\n        )\n\n        return unicodeScalars.allSatisfy { allowed.contains($0) }\n    }\n\n    /// Checks if a string contains only valid URL characters.\n    ///\n    /// Allowed: unreserved (`A–Z a–z 0–9 - . _ ~`), reserved\n    /// (`:/?#[]@!$&'()*+,;=`), and `%` for encoding.\n    ///\n    /// - Returns: `true` if all characters are valid, otherwise `false`.\n    func containsOnlyValidPathCharacters() -> Bool {\n        let disallowed = CharacterSet(\n            charactersIn: \"%?#&=\"\n        )\n        return unicodeScalars.allSatisfy { !disallowed.contains($0) }\n    }\n\n}\n"
  },
  {
    "path": "Sources/ToucanCore/Extensions/URL+Extensions.swift",
    "content": "//\n//  URL+Extensions.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 17..\n//\n\nimport Foundation\n\npublic extension URL {\n    /// Returns a new URL by appending the given path component if it is non-nil and not empty.\n    ///\n    /// This method is useful when working with optional path components where you want to\n    /// conditionally append the value only if it's meaningful (i.e., not `nil` or an empty string).\n    ///\n    /// - Parameter path: An optional string representing the path component to append.\n    /// - Returns: A new `URL` with the appended path component if valid; otherwise, the original URL.\n    ///\n    /// ## Example\n    /// ```swift\n    /// let baseURL = URL(string: \"https://example.com/api\")!\n    /// let endpoint: String? = \"users\"\n    /// let fullURL = baseURL.appendingPathIfPresent(endpoint)\n    /// // fullURL: https://example.com/api/users\n    /// ```\n    func appendingPathIfPresent(_ path: String?) -> URL {\n        guard let path, !path.isEmpty else {\n            return self\n        }\n        return appending(path: path)\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanCore/GeneratorInfo.swift",
    "content": "//\n//  GeneratorInfo.swift\n//  Toucan\n//\n//  Created by Viasz-Kádi Ferenc on 2025. 03. 19..\n//\n\nimport _GitCommitHash\nimport Version\n\npublic extension GeneratorInfo {\n\n    /// Returns the most current version of the generator.\n    static var current: Self {\n        .v1_0_0\n    }\n}\n\n/// List available releases here\nextension GeneratorInfo {\n    static let v1_0_0 = GeneratorInfo(version: \"1.0.0\")\n    static let v1_0_0_rc_1 = GeneratorInfo(version: \"1.0.0-rc.1\")\n    static let v1_0_0_beta_6 = GeneratorInfo(version: \"1.0.0-beta.6\")\n    static let v1_0_0_beta_5 = GeneratorInfo(version: \"1.0.0-beta.5\")\n    static let v1_0_0_beta_4 = GeneratorInfo(version: \"1.0.0-beta.4\")\n    static let v1_0_0_beta_3 = GeneratorInfo(version: \"1.0.0-beta.3\")\n    static let v1_0_0_beta_2 = GeneratorInfo(version: \"1.0.0-beta.2\")\n    static let v1_0_0_beta_1 = GeneratorInfo(version: \"1.0.0-beta.1\")\n}\n\n/// Metadata describing the content generator, including its name, version, and homepage link.\npublic struct GeneratorInfo: Codable, Sendable {\n\n    /// The name of the generator.\n    public let name: String\n\n    /// The version (e.g., `\"1.0.0\"`, `\"1.0.0-beta.4\"`).\n    public let release: Version\n\n    /// The git commit hash based on the SPM context.\n    public var gitCommitHash: String {\n        .init(cString: git_commit_hash())\n    }\n\n    /// The complete version information based on the version and the git commit hash\n    public var version: String {\n        \"\\(release.description) (\\(gitCommitHash))\"\n    }\n\n    /// A URL pointing to the generator’s homepage or documentation.\n    public let link: String\n\n    /// Initializes a generator metadata instance.\n    ///\n    /// - Parameters:\n    ///   - name: The name of the generator (defaults to `\"Toucan\"`).\n    ///   - version: The generator version string.\n    ///   - link: A link to the project or documentation (defaults to GitHub).\n    init(\n        name: String = \"Toucan\",\n        version: String,\n        link: String = \"https://github.com/toucansites/toucan\"\n    ) {\n        self.name = name\n        self.release = Version(version)!\n        self.link = link\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanCore/Logger.swift",
    "content": "//\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 types that can provide structured metadata for logging.\n///\n/// Conforming types expose a dictionary of metadata values used to enrich log messages.\npublic protocol LoggerMetadataRepresentable {\n    /// A dictionary of key-value pairs representing structured logging metadata.\n    ///\n    /// This metadata can be used to provide additional context in log output.\n    var logMetadata: [String: Logger.MetadataValue] { get }\n}\n"
  },
  {
    "path": "Sources/ToucanCore/ToucanError.swift",
    "content": "//\n//  ToucanError.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 20..\n//\n\nimport Foundation\n\n/// A protocol for custom errors used in the Toucan framework.\n///\n/// Provides properties for logging, user-facing messages, and a method\n/// to format nested error messages in a readable stack-like format.\npublic protocol ToucanError: Error {\n    /// A developer-facing error description used for logging purposes.\n    var logMessage: String { get }\n    /// A simplified error message suitable for display to end users.\n    var userFriendlyMessage: String { get }\n    /// A list of underlying errors, useful for representing error hierarchies.\n    var underlyingErrors: [Error] { get }\n    /// Generates a readable stack-like message of the error and any underlying errors.\n    ///\n    /// - Returns: A formatted string detailing the error structure.\n    func logMessageStack() -> String\n\n    /// Searches for an error of a specific type in the error hierarchy.\n    ///\n    /// This method traverses the list of underlying errors and attempts to cast\n    /// each one to the specified error type `T`. If a match is found, it is returned.\n    /// The search is recursive and will descend into nested `ToucanError`s.\n    ///\n    /// - Parameter errorType: The type of error to search for.\n    /// - Returns: An instance of the specified error type if found, otherwise `nil`.\n    func lookup<T: Error>(\n        _ errorType: T.Type\n    ) -> T?\n\n    /// Searches for a specific associated value in the error hierarchy using a custom matcher.\n    ///\n    /// This method first attempts to locate an error of type `T`, and if successful,\n    /// applies the provided matcher closure to extract an associated value.\n    ///\n    /// - Parameter t: A closure that takes an error of type `T` and returns an associated value of type `V?`.\n    /// - Returns: The extracted associated value if found, otherwise `nil`.\n    func lookup<T: Error, V>(\n        _ t: (T) -> V?\n    ) -> V?\n}\n\npublic extension ToucanError {\n    /// Searches for an error of a specific type in the error hierarchy.\n    ///\n    /// This method traverses the list of underlying errors and attempts to cast\n    /// each one to the specified error type `T`. If a match is found, it is returned.\n    /// The search is recursive and will descend into nested `ToucanError`s.\n    ///\n    /// - Parameter errorType: The type of error to search for.\n    /// - Returns: An instance of the specified error type if found, otherwise `nil`.\n    func lookup<T: Error>(\n        _ errorType: T.Type\n    ) -> T? {\n        for error in underlyingErrors {\n            if let match = error as? T {\n                return match\n            }\n            if let match = (error as ToucanError).lookup(errorType) {\n                return match\n            }\n        }\n        return nil\n    }\n\n    /// Searches for a specific associated value in the error hierarchy using a custom matcher.\n    ///\n    /// This method first attempts to locate an error of type `T`, and if successful,\n    /// applies the provided matcher closure to extract an associated value.\n    ///\n    /// - Parameter t: A closure that takes an error of type `T` and returns an associated value of type `V?`.\n    /// - Returns: The extracted associated value if found, otherwise `nil`.\n    func lookup<T: Error, V>(\n        _ t: (T) -> V?\n    ) -> V? {\n        lookup(T.self).flatMap(t)\n    }\n}\n\n/// Conforms `NSError` to the `ToucanError` protocol, providing\n/// default implementations for logging and user-friendly messages.\nextension NSError: ToucanError {\n    /// A detailed log message composed of the domain, code, and localized description.\n    public var logMessage: String {\n        \"\\(domain):\\(code) - \\(localizedDescription)\"\n    }\n\n    /// A user-facing message derived from the localized description.\n    public var userFriendlyMessage: String {\n        \"\\(localizedDescription)\"\n    }\n}\n\n/// Provides default implementations for `ToucanError` protocol\n/// including empty `underlyingErrors` and a recursive `logMessageStack`.\npublic extension ToucanError {\n    /// A default empty list of underlying errors. Can be overridden by conforming types to provide error hierarchies.\n    var underlyingErrors: [Error] { [] }\n\n    /// Recursively builds a string that describes the error and its underlying errors in a readable format.\n    ///\n    /// - Returns: A formatted stack-like string representing the error and any nested underlying errors.\n    func logMessageStack() -> String {\n        format(error: self)\n    }\n\n    /// Recursively formats an error and its underlying errors into a structured log message.\n    ///\n    /// - Parameters:\n    ///   - error: The error to format.\n    ///   - prefix: The current indentation prefix.\n    ///   - isLast: Indicates whether the error is the last in its group.\n    /// - Returns: A formatted string representing the error hierarchy.\n    private func format(\n        error: Error,\n        prefix: String = \"\",\n        isLast: Bool = true\n    ) -> String {\n        let type = type(of: error)\n\n        var message: String\n        var underlyingErrors: [Error]\n        switch error {\n        case let e as ToucanError:\n            message = e.logMessage\n            underlyingErrors = e.underlyingErrors\n        case let e as LocalizedError:\n            message = e.localizedDescription\n            underlyingErrors = []\n        default:\n            message = \"\\(error)\"\n            underlyingErrors = []\n        }\n\n        let branch = prefix.isEmpty ? \"\" : (isLast ? \"└─ \" : \"├─ \")\n        var output = \"\\(prefix)\\(branch)\\(type): \\\"\\(message)\\\"\\n\"\n        let childPrefix = prefix + (isLast ? \"    \" : \"│   \")\n\n        let childCount = underlyingErrors.count\n        for (idx, error) in underlyingErrors.enumerated() {\n            let lastChild = (idx == childCount - 1)\n            output += format(\n                error: error,\n                prefix: childPrefix,\n                isLast: lastChild\n            )\n        }\n\n        return output\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanMarkdown/Markdown/HTML.swift",
    "content": "//\n//  HTML.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 02. 19..\n//\n\nstruct HTML {\n    enum TagType {\n        case standard\n        case short\n    }\n\n    struct Attribute {\n        var key: String\n        var value: String\n    }\n\n    var name: String\n    var type: TagType\n    var attributes: [Attribute]\n    var contents: String?\n\n    init(\n        name: String,\n        type: TagType = .standard,\n        attributes: [Attribute] = [],\n        contents: String? = nil\n    ) {\n        self.name = name\n        self.type = type\n        self.attributes = attributes\n        self.contents = contents\n    }\n\n    func render() -> String {\n        let attributeString =\n            attributes\n            .map { #\"\\#($0.key)=\"\\#($0.value)\"\"# }\n            .joined(separator: \" \")\n\n        let tag = [name, attributeString]\n            .filter { !$0.isEmpty }\n            .joined(separator: \" \")\n\n        var result = \"<\\(tag)>\"\n        result += contents ?? \"\"\n        if type == .standard {\n            result += \"</\\(name)>\"\n        }\n        return result\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanMarkdown/Markdown/HTMLVisitor.swift",
    "content": "//\n//  HTMLVisitor.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 02. 19..\n//\n\nimport Logging\nimport Markdown\nimport ToucanCore\n\n/// NOTE: https://www.markdownguide.org/basic-syntax/\n\nprivate extension String {\n\n    func escapeAngleBrackets() -> String {\n        replacing(\n            [\n                #\"<\"#: #\"&lt;\"#,\n                #\">\"#: #\"&gt;\"#,\n                    // #\"&\"#: #\"&amp;\"#,\n                    // #\"'\"#: #\"&apos;\"#,\n                    // #\"\"\"#: #\"&quot;\"#,\n            ]\n        )\n    }\n}\n\nprivate extension Markup {\n    var isInsideList: Bool {\n        self is ListItemContainer || parent?.isInsideList == true\n    }\n}\n\nprivate extension [DirectiveArgument] {\n    func getFirstValueBy(key name: String) -> String? {\n        first(where: { $0.name == name })?.value\n    }\n}\n\nstruct HTMLVisitor: MarkupVisitor {\n    typealias Result = String\n\n    var customBlockDirectives: [MarkdownBlockDirective]\n    var paragraphStyles: [String: [String]]\n    var logger: Logger\n    var slug: String\n    var assetsPath: String\n    var baseURL: String\n\n    init(\n        blockDirectives: [MarkdownBlockDirective] = [],\n        paragraphStyles: [String: [String]],\n        slug: String,\n        assetsPath: String,\n        baseURL: String,\n        logger: Logger = .subsystem(\"html-visitor\")\n    ) {\n        self.customBlockDirectives = blockDirectives\n        self.paragraphStyles = paragraphStyles\n        self.slug = slug\n        self.assetsPath = assetsPath\n        self.baseURL = baseURL\n        self.logger = logger\n    }\n\n    // MARK: - visitor functions\n\n    private mutating func visit(\n        _ children: MarkupChildren\n    ) -> Result {\n        var result = \"\"\n        for child in children {\n            result += visit(child)\n        }\n        return result\n    }\n\n    mutating func defaultVisit(\n        _ markup: any Markup\n    ) -> Result {\n        visit(markup.children)\n    }\n\n    mutating func visitText(\n        _ text: Text\n    ) -> Result {\n        text.plainText\n    }\n\n    mutating func visitHTMLBlock(\n        _ html: HTMLBlock\n    ) -> Result {\n        html.rawHTML  //.escapeAngleBrackets()\n    }\n\n    mutating func visitInlineHTML(\n        _ inlineHTML: InlineHTML\n    ) -> Result {\n        inlineHTML.rawHTML.escapeAngleBrackets()\n    }\n\n    // MARK: - simple HTML elements\n\n    mutating func visitSoftBreak(\n        _: SoftBreak\n    ) -> Result {\n        HTML(name: \"br\", type: .short).render()\n    }\n\n    mutating func visitLineBreak(\n        _: LineBreak\n    ) -> Result {\n        HTML(name: \"br\", type: .short).render()\n    }\n\n    mutating func visitThematicBreak(\n        _: ThematicBreak\n    ) -> Result {\n        HTML(name: \"hr\", type: .short).render()\n    }\n\n    mutating func visitListItem(\n        _ listItem: ListItem\n    ) -> Result {\n        HTML(name: \"li\", contents: visit(listItem.children)).render()\n    }\n\n    mutating func visitOrderedList(\n        _ orderedList: OrderedList\n    ) -> Result {\n        var attributes: [HTML.Attribute] = []\n        if orderedList.startIndex > 1 {\n            attributes.append(\n                .init(\n                    key: \"start\",\n                    value: String(\n                        orderedList.startIndex\n                    )\n                )\n            )\n        }\n        return HTML(\n            name: \"ol\",\n            attributes: attributes,\n            contents: visit(orderedList.children)\n        )\n        .render()\n    }\n\n    mutating func visitUnorderedList(\n        _ unorderedList: UnorderedList\n    ) -> Result {\n        HTML(name: \"ul\", contents: visit(unorderedList.children)).render()\n    }\n\n    mutating func visitInlineCode(\n        _ inlineCode: InlineCode\n    ) -> Result {\n        HTML(\n            name: \"code\",\n            contents: inlineCode.code.escapeAngleBrackets()\n        )\n        .render()\n    }\n\n    mutating func visitEmphasis(\n        _ emphasis: Emphasis\n    ) -> Result {\n        HTML(name: \"em\", contents: visit(emphasis.children)).render()\n    }\n\n    mutating func visitStrong(\n        _ strong: Strong\n    ) -> Result {\n        HTML(name: \"strong\", contents: visit(strong.children)).render()\n    }\n\n    mutating func visitStrikethrough(\n        _ strikethrough: Strikethrough\n    ) -> Result {\n        HTML(name: \"s\", contents: visit(strikethrough.children)).render()\n    }\n\n    mutating func visitParagraph(\n        _ paragraph: Paragraph\n    ) -> Result {\n        let filterBlocks =\n            customBlockDirectives\n            .filter { $0.removesChildParagraph ?? false }\n            .map(\\.name)\n\n        if let block = paragraph.parent as? BlockDirective,\n            filterBlocks.contains(block.name.lowercased())\n        {\n            return visit(paragraph.children)\n        }\n        /// if the parent is a list element, we don't need to render the p tag\n        if paragraph.isInsideList {\n            return visit(paragraph.children)\n        }\n        return HTML(name: \"p\", contents: visit(paragraph.children)).render()\n    }\n\n    mutating func visitBlockQuote(\n        _ blockQuote: BlockQuote\n    ) -> Result {\n        var paragraphCount = 0\n        var otherCount = 0\n\n        var type: String?\n        var dropCount = 0\n\n        for i in blockQuote.children {\n            if let p = i as? Paragraph {\n                paragraphCount += 1\n                let text = p.plainText.lowercased()\n\n                typeLoop: for (typeValue, prefixes) in paragraphStyles {\n                    for prefix in prefixes {\n                        let fullPrefix = \"\\(prefix): \".lowercased()\n                        if text.hasPrefix(fullPrefix) {\n                            type = typeValue\n                            dropCount = fullPrefix.count\n                            break typeLoop\n                        }\n                    }\n                }\n            }\n            else {\n                otherCount += 1\n            }\n        }\n        guard let type, otherCount == 0, paragraphCount == 1 else {\n            return HTML(\n                name: \"blockquote\",\n                contents: visit(blockQuote.children)\n            )\n            .render()\n        }\n        let paragraph = visit(blockQuote.children)\n        let pTagCount = 3\n        let contents =\n            paragraph.prefix(pTagCount)\n            + paragraph.dropFirst(pTagCount).dropFirst(dropCount)\n        return HTML(\n            name: \"blockquote\",\n            attributes: [\n                .init(key: \"class\", value: type)\n            ],\n            contents: String(contents)\n        )\n        .render()\n    }\n\n    mutating func visitCodeBlock(\n        _ codeBlock: CodeBlock\n    ) -> Result {\n\n        var attributes: [HTML.Attribute] = []\n        if let language = codeBlock.language {\n            attributes.append(\n                .init(\n                    key: \"class\",\n                    value: \"language-\\(language.lowercased())\"\n                )\n            )\n        }\n        let code = HTML(\n            name: \"code\",\n            attributes: attributes,\n            contents: codeBlock.code\n                .escapeAngleBrackets()\n                .replacing(\n                    [\n                        #\"/*!*/\"#: #\"<span class=\"highlight\">\"#,\n                        #\"/*.*/\"#: \"</span>\",\n                    ]\n                )\n        )\n        .render()\n\n        return HTML(name: \"pre\", contents: code).render()\n    }\n\n    mutating func visitHeading(\n        _ heading: Heading\n    ) -> Result {\n        var attributes: [HTML.Attribute] = []\n        if [2, 3].contains(heading.level) {\n            let fragment = heading.plainText.lowercased().slugify()\n            let id = HTML.Attribute(key: \"id\", value: \"\\(fragment)\")\n            attributes.append(id)\n        }\n        return HTML(\n            name: \"h\\(heading.level)\",\n            attributes: attributes,\n            contents: visit(heading.children)\n        )\n        .render()\n    }\n\n    mutating func visitLink(\n        _ link: Link\n    ) -> Result {\n        var attributes: [HTML.Attribute] = []\n\n        if let destination = link.destination {\n            let anchorPrefix = \"#[name]\"\n            if destination.hasPrefix(anchorPrefix) {\n                attributes.append(\n                    .init(\n                        key: \"name\",\n                        value: String(destination.dropFirst(anchorPrefix.count))\n                    )\n                )\n            }\n            else {\n                var hrefDestination = destination\n                if destination.hasPrefix(\"/\") {\n                    hrefDestination =\n                        \"\\(baseURL.ensureTrailingSlash())\\(destination.dropFirst())\"\n                }\n                attributes.append(\n                    .init(\n                        key: \"href\",\n                        value: hrefDestination\n                    )\n                )\n            }\n\n            if !destination.hasPrefix(\".\"),\n                !destination.hasPrefix(\"/\"),\n                !destination.hasPrefix(\"#\")\n            {\n                attributes.append(\n                    .init(\n                        key: \"target\",\n                        value: \"_blank\"\n                    )\n                )\n            }\n        }\n\n        return HTML(\n            name: \"a\",\n            attributes: attributes,\n            contents: visit(link.children)\n        )\n        .render()\n    }\n\n    mutating func visitImage(_ image: Image) -> Result {\n        guard let source = image.source, !source.isEmpty else {\n            return \"\"\n        }\n        let imagePath = source.resolveAsset(\n            baseURL: baseURL,\n            assetsPath: assetsPath,\n            slug: slug\n        )\n        var attributes: [HTML.Attribute] = [\n            .init(key: \"src\", value: imagePath),\n            .init(key: \"alt\", value: image.plainText),\n        ]\n        if let title = image.title {\n            attributes.append(\n                .init(key: \"title\", value: title)\n            )\n        }\n        return HTML(\n            name: \"img\",\n            type: .short,\n            attributes: attributes\n        )\n        .render()\n    }\n\n    // MARK: - table\n\n    mutating func visitTable(\n        _ table: Table\n    ) -> Result {\n        HTML(name: \"table\", contents: visit(table.children)).render()\n    }\n\n    mutating func visitTableHead(\n        _ tableHead: Table.Head\n    ) -> Result {\n        HTML(name: \"thead\", contents: visit(tableHead.children)).render()\n    }\n\n    mutating func visitTableBody(\n        _ tableBody: Table.Body\n    ) -> Result {\n        HTML(name: \"tbody\", contents: visit(tableBody.children)).render()\n    }\n\n    mutating func visitTableRow(\n        _ tableRow: Table.Row\n    ) -> Result {\n        HTML(name: \"tr\", contents: visit(tableRow.children)).render()\n    }\n\n    mutating func visitTableCell(\n        _ tableCell: Table.Cell\n    ) -> Result {\n        HTML(name: \"td\", contents: visit(tableCell.children)).render()\n    }\n\n    // MARK: - custom block directives\n\n    mutating func visitBlockDirective(\n        _ blockDirective: BlockDirective\n    ) -> Result {\n\n        var parseErrors = [DirectiveArgumentText.ParseError]()\n        var arguments: [DirectiveArgument] = []\n        let blockName = blockDirective.name.lowercased()\n        if !blockDirective.argumentText.isEmpty {\n            arguments = blockDirective.argumentText.parseNameValueArguments(\n                parseErrors: &parseErrors\n            )\n        }\n\n        let block = customBlockDirectives.first {\n            $0.name.lowercased() == blockName.lowercased()\n        }\n        guard let block else {\n            logger.warning(\n                \"Unrecognized block directive: `\\(blockName)`\",\n                metadata: [\n                    \"name\": .string(blockName)\n                ]\n            )\n            return \"\"\n        }\n\n        guard parseErrors.isEmpty else {\n            let errors =\n                parseErrors.map { error -> String in\n                    switch error {\n                    case let .duplicateArgument(name, _, _):\n                        return \"Duplicate argument: `\\(name)`.\"\n                    case let .missingExpectedCharacter(char, _):\n                        return \"Misisng expected character: `\\(char)`.\"\n                    case let .unexpectedCharacter(char, _):\n                        return \"Unexpected character: `\\(char)`.\"\n                    }\n                }\n                .joined(separator: \", \")\n\n            logger.warning(\n                \"\\(errors)\",\n                metadata: [\n                    \"name\": .string(blockName)\n                ]\n            )\n            return \"\"\n        }\n\n        var parameters: [String: String] = [:]\n        for p in block.parameters ?? [] {\n            if p.required ?? false {\n                if let v = arguments.getFirstValueBy(key: p.label) {\n                    parameters[p.label] = v\n                }\n                else {\n                    logger.warning(\n                        \"Parameter `\\(p.label)` for `\\(block.name)` is required.\",\n                        metadata: [\n                            \"name\": .string(blockName)\n                        ]\n                    )\n                }\n            }\n            else {\n                let v =\n                    arguments.getFirstValueBy(key: p.label) ?? p.default ?? \"\"\n\n                parameters[p.label] = v\n            }\n        }\n\n        let templateParams = parameters.mapKeys { \"{{\\($0)}}\" }\n\n        if let parent = block.requiresParentDirective, !parent.isEmpty {\n            guard\n                let p = blockDirective.parent as? BlockDirective,\n                p.name.lowercased() == parent.lowercased()\n            else {\n                logger.warning(\n                    \"Block directive `\\(block.name)` requires parent block `\\(parent)`\",\n                    metadata: [\n                        \"name\": .string(blockName)\n                    ]\n                )\n                return \"\"\n            }\n        }\n\n        if let output = block.output {\n            var contents = \"\"\n            for child in blockDirective.children {\n                contents += visit(child)\n            }\n\n            var params = templateParams\n            params[\"{{contents}}\"] = contents\n\n            return output.replacing(params)\n        }\n\n        if let name = block.tag {\n            let attributes: [HTML.Attribute] =\n                block.attributes?\n                .map { a in\n                    .init(\n                        key: a.name,\n                        value: a.value.replacing(templateParams)\n                    )\n                } ?? []\n\n            return HTML(\n                name: name,\n                attributes: attributes,\n                contents: visit(blockDirective.children)\n            )\n            .render()\n        }\n        return \"\"\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanMarkdown/Markdown/MarkdownBlockDirective.swift",
    "content": "//\n//  MarkdownBlockDirective.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 02. 19..\n//\n\n/// A representation of a custom block directive in Markdown, used for extending Markdown syntax with special tags or behaviors.\npublic struct MarkdownBlockDirective: Codable, Equatable {\n    /// Defines a configurable parameter for a directive, which may be required and have a default value.\n    public struct Parameter: Codable, Equatable {\n        /// The label of the parameter.\n        public var label: String\n\n        /// Indicates whether the parameter is required. Defaults to `nil` (optional).\n        public var required: Bool?\n\n        /// A default value for the parameter, used if it is not explicitly specified in the directive.\n        public var `default`: String?\n\n        /// Initializes a `Parameter` for a directive.\n        ///\n        /// - Parameters:\n        ///   - label: The name of the parameter.\n        ///   - isRequired: Indicates if the parameter must be provided.\n        ///   - defaultValue: A fallback value if none is provided.\n        public init(\n            label: String,\n            isRequired: Bool? = nil,\n            defaultValue: String? = nil\n        ) {\n            self.label = label\n            self.required = isRequired\n            self.default = defaultValue\n        }\n    }\n\n    /// Represents a static HTML attribute that will be rendered on the directive's HTML tag.\n    public struct Attribute: Codable, Equatable {\n\n        /// The name of the HTML attribute (e.g., `class`, `id`).\n        public var name: String\n\n        /// The corresponding value of the attribute.\n        public var value: String\n\n        /// Initializes an `Attribute` for the rendered directive HTML tag.\n        ///\n        /// - Parameters:\n        ///   - name: The attribute key.\n        ///   - value: The attribute value.\n        public init(\n            name: String,\n            value: String\n        ) {\n            self.name = name\n            self.value = value\n        }\n    }\n\n    /// The name of the directive (e.g., `\"note\"`, `\"warning\"`, `\"info\"`).\n    public var name: String\n\n    /// A list of supported parameters for the directive.\n    public var parameters: [Parameter]?\n\n    /// If specified, this directive must appear within another directive of the given name.\n    public var requiresParentDirective: String?\n\n    /// Indicates whether child paragraphs should be removed from the HTML output. Defaults to `nil`.\n    public var removesChildParagraph: Bool?\n\n    /// The HTML tag to render (e.g., `\"div\"`, `\"section\"`, `\"aside\"`).\n    public var tag: String?\n\n    /// Static attributes to apply to the rendered HTML tag.\n    public var attributes: [Attribute]?\n\n    /// Custom output HTML string that overrides default rendering behavior, if provided.\n    public var output: String?\n\n    /// Initializes a `MarkdownBlockDirective`.\n    ///\n    /// - Parameters:\n    ///   - name: The directive's name.\n    ///   - parameters: Optional list of accepted parameters.\n    ///   - requiresParentDirective: Name of a parent directive this one must reside within.\n    ///   - removesChildParagraph: Whether to exclude child `<p>` tags during rendering.\n    ///   - tag: HTML tag to be generated.\n    ///   - attributes: HTML attributes to apply.\n    ///   - output: Optional custom HTML output template.\n    public init(\n        name: String,\n        parameters: [Parameter]? = nil,\n        requiresParentDirective: String? = nil,\n        removesChildParagraph: Bool? = nil,\n        tag: String? = nil,\n        attributes: [Attribute]? = nil,\n        output: String? = nil\n    ) {\n        self.name = name\n        self.parameters = parameters\n        self.requiresParentDirective = requiresParentDirective\n        self.removesChildParagraph = removesChildParagraph\n        self.tag = tag\n        self.attributes = attributes\n        self.output = output\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanMarkdown/Markdown/MarkdownToHTMLRenderer.swift",
    "content": "//\n//  MarkdownToHTMLRenderer.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 02. 19..\n//\n\nimport Logging\nimport Markdown\nimport ToucanCore\n\n/// A renderer that converts Markdown text to HTML, with support for custom block directives and paragraph styling.\npublic struct MarkdownToHTMLRenderer {\n    /// Custom block directives to extend Markdown syntax.\n    public let customBlockDirectives: [MarkdownBlockDirective]\n\n    /// A collection of paragraph styles.\n    public let paragraphStyles: [String: [String]]\n\n    /// Logger instance\n    public let logger: Logger\n\n    /// Initializes a `MarkdownToHTMLRenderer`.\n    ///\n    /// - Parameters:\n    ///   - customBlockDirectives: A list of custom Markdown block directives to parse during rendering.\n    ///   - paragraphStyles: The paragraph styles configuration for styling rendered HTML.\n    ///   - logger: A logger instance for logging. Defaults to a logger labeled \"MarkdownToHTMLRenderer\".\n    public init(\n        customBlockDirectives: [MarkdownBlockDirective] = [],\n        paragraphStyles: [String: [String]] = [:],\n        logger: Logger = .subsystem(\"markdown-to-html-renderer\")\n    ) {\n        self.customBlockDirectives = customBlockDirectives\n        self.paragraphStyles = paragraphStyles\n        self.logger = logger\n    }\n\n    // MARK: - render api\n\n    /// Renders the provided Markdown string to an HTML string.\n    ///\n    /// - Parameters:\n    ///   - markdown: The input Markdown text to render.\n    ///   - slug: A slug identifier used for generating.\n    ///   - assetsPath: The path to the assets folder used for resource resolution.\n    ///   - baseURL: The base URL used to resolve relative links within the Markdown.\n    ///\n    /// - Returns: A fully rendered HTML string.\n    public func renderHTML(\n        markdown: String,\n        slug: String,\n        assetsPath: String,\n        baseURL: String\n    ) -> String {\n        // Create a Markdown document, enabling block directives if any are provided.\n        let document = Document(\n            parsing: markdown,\n            options: !customBlockDirectives.isEmpty\n                ? [.parseBlockDirectives] : []\n        )\n\n        // Initialize the HTML visitor with the current configuration.\n        var htmlVisitor = HTMLVisitor(\n            blockDirectives: customBlockDirectives,\n            paragraphStyles: paragraphStyles,\n            slug: slug,\n            assetsPath: assetsPath,\n            baseURL: baseURL\n        )\n\n        // Generate HTML by visiting the document tree.\n        return htmlVisitor.visitDocument(document)\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanMarkdown/MarkdownRenderer.swift",
    "content": "//\n//  MarkdownRenderer.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 02. 20..\n//\n\nimport Logging\nimport ToucanCore\n\n/// A comprehensive content processing engine that renders Markdown content to HTML,\n/// applies transformations, computes reading time, and generates an outline structure.\npublic struct MarkdownRenderer {\n\n    /// Holds all the settings required for rendering and processing content.\n    public struct Configuration {\n        /// Configuration specific to Markdown processing.\n        public struct Markdown {\n\n            /// Custom block directives to extend the Markdown grammar.\n            public var customBlockDirectives: [MarkdownBlockDirective]\n\n            //\n\n            /// Initializes a Markdown configuration.\n            public init(\n                customBlockDirectives: [MarkdownBlockDirective]\n            ) {\n                self.customBlockDirectives = customBlockDirectives\n            }\n        }\n\n        /// Configuration for outlining logic, such as which heading levels to parse.\n        public struct Outline {\n\n            /// Which heading levels to include in the parsed outline.\n            public var levels: [Int]\n\n            //\n\n            /// Initializes an Outline configuration.\n            public init(\n                levels: [Int]\n            ) {\n                self.levels = levels\n            }\n        }\n\n        /// Configuration for estimating reading time.\n        public struct ReadingTime {\n\n            /// Estimated words per minute reading speed.\n            public var wordsPerMinute: Int\n\n            //\n\n            /// Initializes a ReadingTime configuration.\n            public init(\n                wordsPerMinute: Int\n            ) {\n                self.wordsPerMinute = wordsPerMinute\n            }\n        }\n\n        /// Markdown-specific rendering options.\n        public var markdown: Markdown\n\n        /// Outline-parsing preferences.\n        public var outline: Outline\n\n        /// Reading time calculation preferences.\n        public var readingTime: ReadingTime\n\n        /// Optional transformation pipeline to apply pre-processing on the input.\n        public var transformerPipeline: TransformerPipeline?\n\n        /// Paragraph styles for customizing the HTML rendering.\n        public var paragraphStyles: [String: [String]]\n\n        /// Initializes a new rendering configuration.\n        ///\n        /// - Parameters:\n        ///   - markdown: Markdown rendering configuration.\n        ///   - outline: Outline extraction preferences.\n        ///   - readingTime: Reading time estimation settings.\n        ///   - transformerPipeline: Optional content transformation pipeline.\n        ///   - paragraphStyles: Block-level style customization for HTML rendering.\n        public init(\n            markdown: Markdown,\n            outline: Outline,\n            readingTime: ReadingTime,\n            transformerPipeline: TransformerPipeline?,\n            paragraphStyles: [String: [String]]\n        ) {\n            self.markdown = markdown\n            self.outline = outline\n            self.readingTime = readingTime\n            self.transformerPipeline = transformerPipeline\n            self.paragraphStyles = paragraphStyles\n        }\n    }\n\n    /// Final output of the rendering pipeline.\n    public struct Output {\n        /// The fully rendered HTML output.\n        public var html: String\n\n        /// Estimated reading time in minutes.\n        public var readingTime: Int\n\n        ///  A hierarchical structure representing the document's headings.\n        public var outline: [Outline]\n    }\n\n    /// Configuration for rendering, including markdown styles, outline levels, and transformation settings.\n    public var configuration: Configuration\n\n    /// Responsible for converting Markdown into HTML with support for custom directives and styling.\n    public var markdownToHTMLRenderer: MarkdownToHTMLRenderer\n\n    /// Parses the rendered HTML to build a heading outline (used for TOC or navigation).\n    public var outlineParser: OutlineParser\n\n    /// Calculates the estimated reading time for a given HTML or Markdown document.\n    public var readingTimeCalculator: ReadingTimeCalculator\n\n    /// Logger for diagnostics and error reporting during rendering.\n    public var logger: Logger\n\n    /// Creates a new `ContentRenderer` instance with the provided configuration, file manager, and logger.\n    ///\n    /// - Parameters:\n    ///   - configuration: Rendering configuration including markdown, outline, and reading time options.\n    ///   - logger: Optional logger for tracking events and issues.\n    public init(\n        configuration: Configuration,\n        logger: Logger = .subsystem(\"content-renderer\")\n    ) {\n        self.configuration = configuration\n\n        self.markdownToHTMLRenderer = MarkdownToHTMLRenderer(\n            customBlockDirectives: configuration.markdown.customBlockDirectives,\n            paragraphStyles: configuration.paragraphStyles\n        )\n\n        self.outlineParser = OutlineParser(\n            levels: configuration.outline.levels\n        )\n\n        self.readingTimeCalculator = ReadingTimeCalculator(\n            wordsPerMinute: configuration.readingTime.wordsPerMinute\n        )\n\n        self.logger = logger\n    }\n\n    /// Processes the input Markdown content, optionally transforms it, renders it as HTML,\n    /// calculates reading time, and generates an outline.\n    ///\n    /// - Parameters:\n    ///   - content: The raw Markdown content to process.\n    ///   - typeAwareID: A unique identifier used for transformation and rendering context.\n    ///   - slug: The slug of the content.\n    ///   - assetsPath: Path to associated assets (e.g., images or includes).\n    ///   - baseURL: The base URL for resolving relative paths or links.\n    ///\n    /// - Returns: A structured `Output` containing HTML, reading time, and outline.\n    public func render(\n        content: String,\n        typeAwareID: String,\n        slug: String,\n        assetsPath: String,\n        baseURL: String\n    ) -> Output {\n        var finalHtml = content\n        var shouldRenderMarkdown = true\n\n        // Step 1: Run transformer pipeline, if defined and non-empty.\n        if let transformerPipeline = configuration.transformerPipeline {\n            if !transformerPipeline.run.isEmpty {\n                shouldRenderMarkdown = transformerPipeline.isMarkdownResult\n                let executor = TransformerExecutor(\n                    pipeline: transformerPipeline\n                )\n                do {\n                    finalHtml = try executor.transform(\n                        contents: finalHtml,\n                        id: typeAwareID,\n                        slug: slug\n                    )\n                }\n                catch {\n                    logger.error(\"\\(String(describing: error))\")\n                }\n            }\n            else {\n                logger.warning(\"Empty transformer pipeline.\")\n            }\n        }\n\n        // Step 2: If the transformer output isn't already HTML, render Markdown to HTML.\n        if shouldRenderMarkdown {\n            finalHtml = markdownToHTMLRenderer.renderHTML(\n                markdown: content,\n                slug: slug,\n                assetsPath: assetsPath,\n                baseURL: baseURL\n            )\n        }\n\n        // Step 3: Calculate reading time and parse outline from HTML.\n        let readingTime = readingTimeCalculator.calculate(for: finalHtml)\n        let outline = outlineParser.parseHTML(finalHtml)\n\n        return .init(\n            html: finalHtml,\n            readingTime: readingTime,\n            outline: outline\n        )\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanMarkdown/Outline/Outline.swift",
    "content": "//\n//  Outline.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 04. 17..\n//\n\n/// A hierarchical representation of an outline element, used for\n/// structuring headings or sections in a document or interface.\npublic struct Outline: Equatable, Codable {\n    /// The depth level of the outline node (e.g., 1 for top-level, 2 for a subheading, etc.).\n    public var level: Int\n\n    /// The display text of the outline entry, such as a heading title.\n    public var text: String\n\n    /// An optional fragment identifier that can be used for navigation (e.g., URL anchors).\n    public var fragment: String?\n\n    /// A list of child outlines, representing nested structure under this node.\n    public var children: [Outline]\n\n    /// Initializes a new `Outline` instance.\n    ///\n    /// - Parameters:\n    ///   - level: The heading level of the outline (e.g., 1 for `h1`, 2 for `h2`, etc.).\n    ///   - text: The display text for this outline item.\n    ///   - fragment: An optional anchor or link target associated with this item.\n    ///   - children: A list of nested `Outline` elements under this item.\n    public init(\n        level: Int,\n        text: String,\n        fragment: String? = nil,\n        children: [Outline] = []\n    ) {\n        self.level = level\n        self.text = text\n        self.fragment = fragment\n        self.children = children\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanMarkdown/Outline/OutlineParser.swift",
    "content": "//\n//  OutlineParser.swift\n//  Toucan\n//\n//  Created by Viasz-Kádi Ferenc on 2024. 10. 14..\n//\n\nimport Logging\nimport SwiftSoup\nimport ToucanCore\n\n/// A parser that extracts heading elements (`<h1>` to `<h6>`) from HTML and converts them into a structured outline.\npublic struct OutlineParser {\n    /// The heading levels (e.g., `[1, 2, 3]` for `<h1>`, `<h2>`, and `<h3>`) to include in the outline.\n    public var levels: [Int]\n\n    /// Logger instance\n    public var logger: Logger\n\n    /// Initializes an `OutlineParser` with optional levels and a logger.\n    ///\n    /// - Parameters:\n    ///   - levels: Heading levels to extract from the HTML. Must be between 1 and 6. Defaults to all (`[1, 2, 3, 4, 5, 6]`).\n    ///   - logger: A `Logger` instance for capturing logs. Defaults to a logger labeled \"OutlineParser\".\n    public init(\n        levels: [Int] = [1, 2, 3, 4, 5, 6],\n        logger: Logger = .subsystem(\"outline-parser\")\n    ) {\n        // Ensure levels are within the valid range of HTML headings.\n        precondition(\n            levels.allSatisfy { 1...6 ~= $0 },\n            \"Values must be between 1 and 6.\"\n        )\n\n        self.levels = levels\n        self.logger = logger\n    }\n\n    /// Converts a single SwiftSoup element into an `Outline` if it corresponds to a valid heading.\n    ///\n    /// - Parameter element: A SwiftSoup `Element` representing a heading node.\n    /// - Returns: An `Outline` instance if the element is a valid heading, otherwise `nil`.\n    /// - Throws: An error if parsing the element fails.\n    func createToC(\n        from element: SwiftSoup.Element\n    ) throws -> Outline? {\n        let text = try element.text()\n\n        let nodeName = element.nodeName()\n        guard\n            nodeName.count > 1,\n            let rawLevel = nodeName.last,\n            let level = Int(String(rawLevel)),\n            (1...6).contains(level)\n        else {\n            return nil\n        }\n\n        var fragment: String?\n        let id = try element.attr(\"id\")\n        if !id.isEmpty {\n            fragment = id\n        }\n\n        return .init(\n            level: level,\n            text: text,\n            fragment: fragment\n        )\n    }\n\n    /// Parses the given HTML string and returns a flat list of `Outline` items corresponding to the specified heading levels.\n    ///\n    /// - Parameter html: A string of HTML content.\n    /// - Returns: An array of `Outline` instances representing the headings found.\n    public func parseHTML(\n        _ html: String\n    ) -> [Outline] {\n        do {\n            // Parse HTML content into a SwiftSoup document.\n            let document = try SwiftSoup.parse(html)\n\n            // Build a CSS selector for the specified heading levels (e.g., \"h1, h2, h3\").\n            let tagSelector = levels.map { \"h\\($0)\" }.joined(separator: \", \")\n\n            // Select and process matching heading elements.\n            let headings = try document.select(tagSelector)\n            return try headings.compactMap { try createToC(from: $0) }\n        }\n        catch let Exception.Error(type, message) {\n            logger.error(\"\\(type) - \\(message)\")\n            return []\n        }\n        catch {\n            logger.error(\"\\(error.localizedDescription)\")\n            return []\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanMarkdown/ReadingTime/ReadingTimeCalculator.swift",
    "content": "//\n//  ReadingTimeCalculator.swift\n//  Toucan\n//\n//  Created by Viasz-Kádi Ferenc on 2024. 10. 15..\n//\n\nimport Logging\nimport ToucanCore\n\n/// A utility to estimate the reading time of a given string of text based on words per minute.\npublic struct ReadingTimeCalculator {\n    /// The number of words assumed to be read per minute.\n    public var wordsPerMinute: Int\n\n    /// Logger instance\n    public var logger: Logger\n\n    /// Initializes a new instance of `ReadingTimeCalculator`.\n    ///\n    /// - Parameters:\n    ///   - wordsPerMinute: The number of words a person can read per minute. Defaults to 238.\n    ///   - logger: A `Logger` instance for logging internal operations. Defaults to a logger labeled \"ReadingTimeCalculator\".\n    public init(\n        wordsPerMinute: Int = 238,\n        logger: Logger = .subsystem(\"reading-time-calculator\")\n    ) {\n        self.wordsPerMinute = wordsPerMinute\n        self.logger = logger\n    }\n\n    /// Calculates the estimated reading time for a given string.\n    ///\n    /// - Parameter string: The input text to estimate reading time for.\n    /// - Returns: An estimated reading time in minutes. Returns at least 1 minute.\n    public func calculate(\n        for string: String\n    ) -> Int {\n        max(string.split(separator: \" \").count / wordsPerMinute, 1)\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanMarkdown/Transformers/ContentTransformer.swift",
    "content": "//\n//  ContentTransformer.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 02. 21..\n//\n\n/// Represents a content transformer command used in a transformation pipeline.\npublic struct ContentTransformer {\n\n    /// The directory path where the executable is located.\n    /// Defaults to `\"/usr/local/bin\"` if not explicitly specified.\n    public var path: String\n\n    /// The name of the executable or script to run.\n    public var name: String\n\n    /// Initializes a new `ContentTransformer` with an optional path and required name.\n    ///\n    /// - Parameters:\n    ///   - path: The directory path to the executable. Defaults to `\"/usr/local/bin\"`.\n    ///   - name: The name of the command-line executable or script.\n    public init(\n        path: String = \"/usr/local/bin\",\n        name: String\n    ) {\n        self.path = path\n        self.name = name\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanMarkdown/Transformers/TransformerExecutor.swift",
    "content": "//\n//  TransformerExecutor.swift\n//  Toucan\n//\n//  Created by Viasz-Kádi Ferenc on 2024. 10. 15..\n//\n\nimport Foundation\nimport Logging\nimport SwiftCommand\nimport ToucanCore\n\n/// Executes a sequence of shell-based transformation commands defined in a `TransformerPipeline`,\n/// allowing content to be programmatically modified.\npublic struct TransformerExecutor {\n    /// The transformation pipeline consisting of commands to execute.\n    public var pipeline: TransformerPipeline\n\n    /// File manager utility for file system interactions, including temp files and cleanup.\n    public var fileManager: FileManager\n\n    /// Logger instance.\n    public var logger: Logger\n\n    /// Initializes a `TransformerExecutor` with a transformation pipeline and file manager.\n    ///\n    /// - Parameters:\n    ///   - pipeline: A sequence of external commands to run for transformation.\n    ///   - fileManager: A file manager abstraction for working with files.\n    ///   - logger: A logger for capturing stdout, stderr, and errors.\n    public init(\n        pipeline: TransformerPipeline,\n        fileManager: FileManager = .default,\n        logger: Logger = .subsystem(\"transformer-executor\")\n    ) {\n        self.pipeline = pipeline\n        self.fileManager = fileManager\n        self.logger = logger\n    }\n\n    /// Transforms the given content string using the defined pipeline.\n    ///\n    /// This function:\n    /// - Saves the content to a temporary file.\n    /// - Executes each command in the pipeline sequentially, modifying the file in place.\n    /// - Captures and logs output and errors.\n    /// - Returns the final transformed content.\n    ///\n    /// - Parameters:\n    ///   - contents: The raw content to be transformed.\n    ///   - id: An identifier used to pass context to the commands.\n    ///   - slug: The slug of the content.\n    ///\n    /// - Throws: Rethrows any error encountered during reading, writing, or transformation.\n    /// - Returns: The final transformed content string.\n    public func transform(\n        contents: String,\n        id: String,\n        slug: String\n    ) throws -> String {\n        // Step 1: Write the content to a temp file\n        let tempDirectoryURL = fileManager.temporaryDirectory\n        let fileName = UUID().uuidString\n        let fileURL = tempDirectoryURL.appendingPathComponent(fileName)\n        try contents.write(to: fileURL, atomically: true, encoding: .utf8)\n\n        // Step 2: Run each command in the transformation pipeline\n        for command in pipeline.run {\n            do {\n                let arguments: [String] = [\n                    \"--id\", id,\n                    \"--file\", fileURL.path,\n                    \"--slug\", slug,\n                ]\n                let commandURL = URL(fileURLWithPath: command.path)\n                    .appendingPathComponent(command.name)\n\n                let command = Command(executablePath: .init(commandURL.path()))\n                    .addArguments(arguments)\n\n                let result = try command.waitForOutput()\n\n                // Log output and errors\n                if !result.stdout.isEmpty {\n                    logger.debug(\"\\(result)\")\n                }\n                if let err = result.stderr, !err.isEmpty {\n                    logger.error(\"\\(err)\")\n                }\n            }\n            catch {\n                logger.error(\"\\(error))\")\n            }\n        }\n\n        // Step 3: Read the transformed contents, clean up, and return\n        do {\n            let finalContents = try String(\n                contentsOf: fileURL,\n                encoding: .utf8\n            )\n            try fileManager.removeItem(at: fileURL)\n            return finalContents\n        }\n        catch {\n            // Ensure cleanup is still performed\n            try fileManager.removeItem(at: fileURL)\n            throw error\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanMarkdown/Transformers/TransformerPipeline.swift",
    "content": "//\n//  TransformerPipeline.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 02. 21..\n//\n\n/// Represents a sequence of content transformers to run before rendering,\n/// along with an indicator of whether the final result is Markdown.\npublic struct TransformerPipeline {\n\n    /// An ordered list of transformers (external commands or scripts) to execute.\n    ///\n    /// Each `ContentTransformer` represents an individual transformation step.\n    public var run: [ContentTransformer]\n\n    /// Indicates whether the final output from this pipeline is expected to be Markdown.\n    ///\n    /// If `false`, the renderer may treat the output as already-formatted HTML or another format.\n    public var isMarkdownResult: Bool\n\n    /// Initializes a new `TransformerPipeline`.\n    ///\n    /// - Parameters:\n    ///   - run: An array of `ContentTransformer` instances to execute.\n    ///   - isMarkdownResult: A flag indicating whether the final output is Markdown. Defaults to `true`.\n    public init(\n        run: [ContentTransformer] = [],\n        isMarkdownResult: Bool = true\n    ) {\n        self.run = run\n        self.isMarkdownResult = isMarkdownResult\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSDK/Behaviors/Behavior.swift",
    "content": "//\n//  Behavior.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 06. 16..\n//\n\nimport struct Foundation.URL\n\n/// A protocol that defines a behavior with a unique identifier\n/// and an operation that runs on a given file URL.\nprotocol Behavior {\n    /// A unique identifier for the behavior.\n    static var id: String { get }\n\n    /// Executes the behavior with the given file URL.\n    ///\n    /// - Parameter fileURL: The URL of the file to process.\n    /// - Returns: A `String` result of the behavior.\n    /// - Throws: An error if the behavior fails.\n    func run(fileURL: URL) throws -> String\n}\n"
  },
  {
    "path": "Sources/ToucanSDK/Behaviors/CompileSASSBehavior.swift",
    "content": "//\n//  CompileSASSBehavior.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 12..\n//\n\nimport DartSass\nimport Foundation\n\nstruct CompileSASSBehavior: Behavior {\n\n    static let id = \"compile-sass\"\n\n    var compiler: Compiler\n\n    init() throws {\n        self.compiler = try .init()\n    }\n\n    /// NOTE: This is horrible... but we can live with it for a while :)\n    private func unsafeSyncCompile(fileURL: URL) -> String {\n        final class Enclosure: @unchecked Sendable {\n            var value: CompilerResults!\n        }\n\n        let semaphore = DispatchSemaphore(value: 0)\n        let enclosure = Enclosure()\n\n        Task {\n            do {\n                enclosure.value =\n                    try await compiler.compile(\n                        fileURL: fileURL\n                    )\n            }\n            catch {\n                fatalError(\"\\(error) - \\(fileURL.path())\")\n            }\n\n            semaphore.signal()\n        }\n\n        semaphore.wait()\n        return enclosure.value.css\n    }\n\n    func run(fileURL: URL) throws -> String {\n        let css = unsafeSyncCompile(fileURL: fileURL)\n\n        return css\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSDK/Behaviors/MinifyCSSBehavior.swift",
    "content": "//\n//  MinifyCSSBehavior.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 12..\n//\n\nimport Foundation\nimport SwiftCSSParser\n\nstruct MinifyCSSBehavior: Behavior {\n\n    static let id = \"minify-css\"\n\n    func run(fileURL: URL) throws -> String {\n        let src = try String(\n            contentsOf: fileURL,\n            encoding: .utf8\n        )\n        let stylesheet = try Stylesheet.parse(from: src)\n        return stylesheet.minified()\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSDK/Content/Content+Query.swift",
    "content": "//\n//  Content+Query.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 01. 31..\n//\n\nimport Foundation\nimport ToucanSource\nimport Logging\n\npublic extension Content {\n    /// Flattens the content's core properties, relations, and metadata into a single dictionary\n    /// for use in filtering, querying, or templating contexts.\n    ///\n    /// - Includes:\n    ///   - All `properties` as defined in the content type\n    ///   - Resolved `relations`, where:\n    ///     - `.one` types return a single identifier (or an empty array if unresolved)\n    ///     - `.many` types return an array of identifiers\n    ///   - Additional metadata:\n    ///     - `\"id\"`: The content's unique identifier\n    ///     - `\"slug\"`: The slug string used for URLs\n    ///     - `\"lastUpdate\"`: Last modification timestamp of the content\n    ///     - `\"iterator\"`: Boolean flag indicating if this content is an iterator item\n    ///\n    /// - Returns: A `[String: AnyCodable]` dictionary representing queryable fields.\n    var queryFields: [String: AnyCodable] {\n        var fields = properties\n\n        // Flatten relational fields by type\n        for (key, relation) in relations {\n            switch relation.type {\n            case .one:\n                if relation.identifiers.isEmpty {\n                    // Default to empty array if no target\n                    fields[key] = .init([])\n                }\n                else {\n                    fields[key] = .init(relation.identifiers[0])  // Single ID\n                }\n            case .many:\n                fields[key] = .init(relation.identifiers)  // Array of IDs\n            }\n        }\n\n        // Append metadata fields\n        fields[SystemPropertyKeys.id.rawValue] = .init(typeAwareID)\n        fields[SystemPropertyKeys.lastUpdate.rawValue] = .init(\n            rawValue.lastModificationDate\n        )\n        fields[SystemPropertyKeys.slug.rawValue] = .init(slug.value)\n        fields[RootContextKeys.iterator.rawValue] = .init(isIterator)\n\n        return fields\n    }\n}\n\npublic extension [Content] {\n    /// Executes a `Query` against the current content collection, applying filtering,\n    /// sorting, and pagination.\n    ///\n    /// - Parameters:\n    ///   - query: The `Query` object containing filtering, ordering, and limit logic.\n    ///   - now: The current timestamp used for time-based filtering.\n    ///   - logger: A `Logger` instance for capturing logs.\n    /// - Returns: A filtered, sorted, and paginated array of `Content` items.\n    func run(\n        query: Query,\n        now: TimeInterval,\n        logger: Logger\n    ) -> [Content] {\n        let contents = filter { query.contentType == $0.type.id }\n        return filter(\n            contents: contents,\n            using: query.resolveFilterParameters(\n                with: [\n                    \"date.now\": .init(now)\n                ]\n            ),\n            logger: logger\n        )\n    }\n\n    /// Filters, sorts, and slices the given content array based on a query.\n    private func filter(\n        contents: [Content],\n        using query: Query,\n        logger: Logger\n    ) -> [Content] {\n        var filteredContents = contents.filter { element in\n            evaluate(condition: query.filter, with: element.queryFields)\n        }\n\n        for order in query.orderBy.reversed() {\n            filteredContents.sort { a, b in\n                let propertyForOrderKey: (Content) -> AnyCodable? = { item in\n                    guard let value = item.properties[order.key] else {\n                        logger.warning(\n                            \"Missing order property key: `\\(order.key)`.\",\n                            metadata: [\n                                \"slug\": .string(item.slug.value),\n                                \"contentType\": .string(query.contentType),\n                            ]\n                        )\n                        return nil\n                    }\n                    return value\n                }\n\n                guard\n                    let valueA = propertyForOrderKey(a),\n                    let valueB = propertyForOrderKey(b)\n                else {\n                    return false\n                }\n\n                return compare(\n                    valueA,\n                    valueB,\n                    ascending: order.direction == .asc\n                )\n            }\n        }\n\n        if let offset = query.offset {\n            filteredContents = Array(filteredContents.dropFirst(offset))\n        }\n\n        if let limit = query.limit {\n            filteredContents = Array(filteredContents.prefix(limit))\n        }\n        return filteredContents\n    }\n\n    /// Recursively evaluates a `Condition` tree against a set of content fields.\n    private func evaluate(\n        condition: Condition?,\n        with props: [String: AnyCodable]\n    ) -> Bool {\n        guard let condition else { return true }\n\n        switch condition {\n        case let .field(key, `operator`, value):\n            guard let fieldValue = props[key] else { return false }\n            return evaluateField(\n                fieldValue: fieldValue,\n                operator: `operator`,\n                value: value\n            )\n\n        case let .and(conditions):\n            return conditions.allSatisfy {\n                evaluate(condition: $0, with: props)\n            }\n\n        case let .or(conditions):\n            return conditions.contains {\n                evaluate(condition: $0, with: props)\n            }\n        }\n    }\n\n    /// Compares two values for equality, supporting multiple types.\n    private func equals(_ valueA: AnyCodable, _ valueB: AnyCodable) -> Bool {\n        if let a = valueA.value(as: Bool.self),\n            let b = valueB.value(as: Bool.self)\n        {\n            return a == b\n        }\n\n        if let a = valueA.value(as: Int.self),\n            let b = valueB.value(as: Int.self)\n        {\n            return a == b\n        }\n\n        if let a = valueA.value(as: Double.self),\n            let b = valueB.value(as: Double.self)\n        {\n            return a == b\n        }\n\n        if let a = valueA.value(as: String.self),\n            let b = valueB.value(as: String.self)\n        {\n            return a == b\n        }\n\n        return false\n    }\n\n    /// Performs numeric or string comparison between two values, with optional inclusiveness.\n    private func compare(\n        _ valueA: AnyCodable,\n        _ valueB: AnyCodable,\n        ascending: Bool,\n        isInclusive: Bool = false\n    ) -> Bool {\n        if let a = valueA.value(as: Int.self),\n            let b = valueB.value(as: Int.self)\n        {\n            return isInclusive\n                ? (ascending ? a <= b : a >= b) : (ascending ? a < b : a > b)\n        }\n\n        if let a = valueA.value(as: Double.self),\n            let b = valueB.value(as: Double.self)\n        {\n            return isInclusive\n                ? (ascending ? a <= b : a >= b) : (ascending ? a < b : a > b)\n        }\n\n        if let a = valueA.value(as: String.self),\n            let b = valueB.value(as: String.self)\n        {\n            return isInclusive\n                ? (ascending ? a <= b : a >= b) : (ascending ? a < b : a > b)\n        }\n\n        return false\n    }\n\n    /// Evaluates a field condition against a value using the provided operator.\n    private func evaluateField(\n        fieldValue: AnyCodable,\n        operator: Operator,\n        value: AnyCodable\n    ) -> Bool {\n        switch `operator` {\n        case .equals: return equals(fieldValue, value)\n\n        case .notEquals: return !equals(fieldValue, value)\n\n        case .lessThan: return compare(fieldValue, value, ascending: true)\n\n        case .greaterThan: return compare(fieldValue, value, ascending: false)\n\n        case .lessThanOrEquals:\n            return compare(\n                fieldValue,\n                value,\n                ascending: true,\n                isInclusive: true\n            )\n\n        case .greaterThanOrEquals:\n            return compare(\n                fieldValue,\n                value,\n                ascending: false,\n                isInclusive: true\n            )\n\n        case .like:\n            return fieldValue.value(as: String.self)?\n                .contains(value.value(as: String.self) ?? \"\") ?? false\n\n        case .caseInsensitiveLike:\n            return fieldValue.value(as: String.self)?\n                .lowercased()\n                .contains(value.value(as: String.self)?.lowercased() ?? \"\")\n                ?? false\n\n        case .in:\n            if let v = fieldValue.value(as: Int.self),\n                let arr = value.value(as: [Int].self)\n            {\n                return arr.contains(v)\n            }\n            if let v = fieldValue.value(as: Double.self),\n                let arr = value.value(as: [Double].self)\n            {\n                return arr.contains(v)\n            }\n            if let v = fieldValue.value(as: String.self),\n                let arr = value.value(as: [String].self)\n            {\n                return arr.contains(v)\n            }\n            return false\n\n        case .contains:\n            if let arr = fieldValue.value(as: [Int].self),\n                let v = value.value(as: Int.self)\n            {\n                return arr.contains(v)\n            }\n            if let arr = fieldValue.value(as: [Double].self),\n                let v = value.value(as: Double.self)\n            {\n                return arr.contains(v)\n            }\n            if let arr = fieldValue.value(as: [String].self),\n                let v = value.value(as: String.self)\n            {\n                return arr.contains(v)\n            }\n            return false\n\n        case .matching:\n            if let arr = fieldValue.value(as: [Int].self),\n                let other = value.value(as: [Int].self)\n            {\n                return !Set(arr).intersection(other).isEmpty\n            }\n            if let arr = fieldValue.value(as: [Double].self),\n                let other = value.value(as: [Double].self)\n            {\n                return !Set(arr).intersection(other).isEmpty\n            }\n            if let arr = fieldValue.value(as: [String].self),\n                let other = value.value(as: [String].self)\n            {\n                return !Set(arr).intersection(other).isEmpty\n            }\n            return false\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSDK/Content/Content.swift",
    "content": "//\n//  Content.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 01. 15..\n//\n\nimport ToucanSource\n\n/// Represents a unit of structured content, with associated metadata, relationships, and rendering information.\npublic struct Content {\n\n    /// The content type definition that describes structure and expected fields.\n    public var type: ContentType\n\n    /// A globally unique string identifier for this content item.\n    /// This value remains constant across contexts and is used for persistence or lookup.\n    public var typeAwareID: String\n\n    /// A URL-friendly slug that identifies the content in paths or links.\n    public var slug: Slug\n\n    /// The raw content representation, usually Markdown or HTML source.\n    public var rawValue: RawContent\n\n    /// A dictionary of properties that hold the parsed field values (e.g., title, date, body).\n    /// Keys are field names as defined in the `ContentType`, and values are dynamically typed.\n    public var properties: [String: AnyCodable]\n\n    /// A dictionary of relations to other content items, keyed by relation name.\n    /// The relation values may include identifiers or full references depending on usage.\n    public var relations: [String: RelationValue]\n\n    /// Arbitrary user-defined metadata not explicitly declared in the content definition.\n    /// These are typically useful for extensibility or plugin features.\n    public var userDefined: [String: AnyCodable]\n\n    /// Optional iterator metadata if the content is generated through iteration (e.g., paginated or list item).\n    public var iteratorInfo: IteratorInfo?\n\n    /// A computed flag indicating whether this content instance was generated via iteration.\n    public var isIterator: Bool { iteratorInfo != nil }\n\n    /// Initializes a new `Content` instance.\n    ///\n    /// - Parameters:\n    ///   - type: Structural schema for this content.\n    ///   - typeAwareID: A unique identifier.\n    ///   - slug: A human-readable URL slug.\n    ///   - rawValue: The unparsed content.\n    ///   - properties: Parsed content fields.\n    ///   - relations: Links to other content.\n    ///   - userDefined: Freeform or plugin-provided metadata.\n    ///   - iteratorInfo: Optional info for repeated or generated content.\n    public init(\n        type: ContentType,\n        typeAwareID: String,\n        slug: Slug,\n        rawValue: RawContent,\n        properties: [String: AnyCodable],\n        relations: [String: RelationValue],\n        userDefined: [String: AnyCodable],\n        iteratorInfo: IteratorInfo?\n    ) {\n        self.type = type\n        self.typeAwareID = typeAwareID\n        self.slug = slug\n        self.rawValue = rawValue\n        self.properties = properties\n        self.relations = relations\n        self.userDefined = userDefined\n        self.iteratorInfo = iteratorInfo\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSDK/Content/ContentResolver.swift",
    "content": "//\n//  ContentResolver.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 21..\n//\n\nimport Foundation\nimport Logging\nimport ToucanCore\nimport ToucanSerialization\nimport ToucanSource\n\nprivate extension Path {\n\n    func getTypeAwareIdentifier() -> String {\n        let newRawPath =\n            value\n            .split(separator: \"/\")\n            .last\n            .map(String.init) ?? \"\"\n        return Path(newRawPath).trimmingBracketsContent()\n    }\n}\n\nenum ContentResolverError: ToucanError {\n    case contentType(ContentTypeResolverError)\n    case missingProperty(String, String)\n    case missingRelation(String, String)\n    case invalidProperty(String, String, String)\n    case invalidSlug(String)\n    case unknown(Error)\n\n    var underlyingErrors: [any Error] {\n        switch self {\n        case let .contentType(error):\n            [error]\n        case .missingProperty:\n            []\n        case .missingRelation:\n            []\n        case .invalidProperty:\n            []\n        case .invalidSlug:\n            []\n        case let .unknown(error):\n            [error]\n        }\n    }\n\n    var logMessage: String {\n        switch self {\n        case .contentType(_):\n            \"Content type related error.\"\n        case let .missingProperty(name, slug):\n            \"Missing property `\\(name)` for content: \\(slug).\"\n        case let .missingRelation(name, slug):\n            \"Missing property `\\(name)` for content: \\(slug).\"\n        case let .invalidProperty(name, value, slug):\n            \"Invalid property `\\(name): \\(value)` for content: \\(slug).\"\n        case let .invalidSlug(slug):\n            \"Invalid slug for content: \\(slug).\"\n        case let .unknown(error):\n            error.localizedDescription\n        }\n    }\n\n    var userFriendlyMessage: String {\n        switch self {\n        case .contentType(_):\n            \"Content type related error.\"\n        case let .missingProperty(name, slug):\n            \"Missing property `\\(name)` for content: `\\(slug)`.\"\n        case let .missingRelation(name, slug):\n            \"Missing property `\\(name)` for content: `\\(slug)`.\"\n        case let .invalidProperty(name, value, slug):\n            \"Invalid property `\\(name): \\(value)` for content: \\(slug).\"\n        case let .invalidSlug(slug):\n            \"Invalid slug for content: \\(slug).\"\n        case .unknown:\n            \"Unknown content conversion error.\"\n        }\n    }\n}\n\nstruct ContentResolver {\n\n    var contentTypeResolver: ContentTypeResolver\n    var encoder: ToucanEncoder\n    var decoder: ToucanDecoder\n    var dateFormatter: ToucanInputDateFormatter\n    var logger: Logger\n\n    init(\n        contentTypeResolver: ContentTypeResolver,\n        encoder: ToucanEncoder,\n        decoder: ToucanDecoder,\n        dateFormatter: ToucanInputDateFormatter,\n        logger: Logger = .subsystem(\"content-resolver\")\n    ) {\n        self.contentTypeResolver = contentTypeResolver\n        self.encoder = encoder\n        self.decoder = decoder\n        self.dateFormatter = dateFormatter\n        self.logger = logger\n    }\n\n    private func rewrite(\n        iteratorID: String,\n        pageIndex: Int,\n        _ value: inout String\n    ) {\n        value = value.replacing([\n            \"{{\\(iteratorID)}}\": String(pageIndex)\n        ])\n    }\n\n    private func rewrite(\n        number: Int,\n        total: Int,\n        _ array: inout [String: AnyCodable]\n    ) {\n        for (key, _) in array {\n            if let stringValue = array[key]?.stringValue() {\n                array[key] = .init(\n                    replace(\n                        in: stringValue,\n                        number: number,\n                        total: total\n                    )\n                )\n            }\n        }\n    }\n\n    private func replace(\n        in value: String,\n        number: Int,\n        total: Int\n    ) -> String {\n        value.replacing([\n            \"{{number}}\": String(number),\n            \"{{total}}\": String(total),\n        ])\n    }\n\n    private func createDictionaryValues(\n        assetKeys: [String],\n        array: [String]\n    ) -> [String: AnyCodable] {\n        var values: [String: AnyCodable] = [:]\n        for i in 0..<array.count {\n            values[assetKeys[i]] = .init(array[i])\n        }\n        return values\n    }\n\n    private func filterFilePaths(\n        from paths: [String],\n        input: Pipeline.Assets.Location\n    ) -> [String] {\n        paths.filter { filePath in\n            guard let url = URL(string: filePath) else {\n                return false\n            }\n\n            let path = url.deletingLastPathComponent().path\n            let name = url.deletingPathExtension().lastPathComponent\n            let ext = url.pathExtension\n\n            let inputPath = input.path ?? \"\"\n            let pathMatches =\n                inputPath == \"*\" || inputPath.isEmpty || path == inputPath\n            let nameMatches =\n                input.name == \"*\" || input.name.isEmpty || name == input.name\n            let extMatches =\n                input.ext == \"*\" || input.ext.isEmpty || ext == input.ext\n            return pathMatches && nameMatches && extMatches\n        }\n    }\n\n    // MARK: - asset behaviors\n\n    private func getNameAndExtension(\n        from path: String\n    ) -> (name: String, ext: String) {\n        let safePath = path.split(separator: \"/\").last.map(String.init) ?? \"\"\n\n        let parts = safePath.split(\n            separator: \".\",\n            omittingEmptySubsequences: false\n        )\n        guard parts.count >= 2 else {\n            return (String(safePath), \"\")  // No extension\n        }\n\n        let ext = String(parts.last!)\n        let filename = parts.dropLast().joined(separator: \".\")\n\n        return (filename, ext)\n    }\n\n    // MARK: - conversion\n\n    func convert(\n        rawContents: [RawContent]\n    ) throws(ContentResolverError) -> [Content] {\n        do {\n            return try rawContents.map {\n                try convert(rawContent: $0)\n            }\n        }\n        catch let error as ContentResolverError {\n            throw error\n        }\n        catch {\n            throw .unknown(error)\n        }\n    }\n\n    // MARK: - error helper\n\n    func getContentType(\n        for origin: Origin,\n        using id: String?\n    ) throws(ContentResolverError) -> ContentType {\n        do {\n            return try contentTypeResolver.getContentType(\n                for: origin,\n                using: id\n            )\n        }\n        catch {\n            throw .contentType(error)\n        }\n    }\n\n    // MARK: - conversion helpers\n\n    func convert(\n        property: Property,\n        rawValue: AnyCodable?,\n        forKey key: String,\n        slug: String\n    ) throws(ContentResolverError) -> AnyCodable? {\n        let value = rawValue ?? property.defaultValue\n\n        switch property.type {\n        case let .date(config):\n            guard\n                let rawDateValue = value?.value(as: String.self)\n            else {\n                throw .invalidProperty(\n                    key,\n                    value?.stringValue() ?? \"nil\",\n                    slug\n                )\n            }\n            guard\n                let date = dateFormatter.date(\n                    from: rawDateValue,\n                    using: config\n                )\n            else {\n                throw .invalidProperty(\n                    key,\n                    value?.stringValue() ?? \"nil\",\n                    slug\n                )\n            }\n            return .init(date.timeIntervalSince1970)\n        default:\n            return value\n        }\n    }\n\n    func convert(\n        rawContent: RawContent\n    ) throws(ContentResolverError) -> Content {\n        let typeID = rawContent.markdown.frontMatter.string(\n            SystemPropertyKeys.type.rawValue\n        )\n\n        let contentType = try getContentType(\n            for: rawContent.origin,\n            using: typeID\n        )\n\n        var properties: [String: AnyCodable] = [:]\n\n        // validate properties\n        let frontMatter = rawContent.markdown.frontMatter\n        let missingProperties = contentType.properties\n            .filter { name, property in\n                let isRequiredButMissing =\n                    property.required && frontMatter[name] == nil\n                let hasNoDefaultValue = property.defaultValue?.value == nil\n                let isNotSystemProperty = !SystemPropertyKeys.allCases\n                    .map { $0.rawValue }\n                    .contains(name)\n\n                return isRequiredButMissing && hasNoDefaultValue\n                    && isNotSystemProperty\n            }\n\n        for name in missingProperties.keys {\n            throw .missingProperty(name, rawContent.origin.slug)\n        }\n\n        /// validate relations\n        let missingRelations = contentType.relations.keys.filter {\n            frontMatter[$0] == nil\n        }\n\n        for name in missingRelations {\n            throw .missingRelation(name, rawContent.origin.slug)\n        }\n\n        // Extrant `id` from front matter or path or fallback to origin path\n        var typeAwareID = rawContent.origin.path.getTypeAwareIdentifier()\n\n        if let id = rawContent.markdown.frontMatter.string(\n            SystemPropertyKeys.id.rawValue\n        ) {\n            typeAwareID = id\n        }\n\n        // Extract `slug` from front matter or fallback to origin slug\n        var slug: String = rawContent.origin.slug\n        if let rawSlug = rawContent.markdown.frontMatter.string(\n            SystemPropertyKeys.slug.rawValue,\n            allowingEmptyValue: true\n        ) {\n            guard rawSlug.containsOnlyValidURLCharacters() else {\n                throw .invalidSlug(rawSlug)\n            }\n            slug = rawSlug  // .slugify()\n        }\n\n        // Convert schema-defined properties\n        for (key, property) in contentType.properties.sorted(by: {\n            $0.key < $1.key\n        }) {\n            var rawValue: AnyCodable?\n\n            switch key {\n            case SystemPropertyKeys.id.rawValue:\n                rawValue = .init(typeAwareID)\n            case SystemPropertyKeys.lastUpdate.rawValue:\n                rawValue = .init(rawContent.lastModificationDate)\n            case SystemPropertyKeys.slug.rawValue:\n                rawValue = .init(slug)\n            case SystemPropertyKeys.type.rawValue:\n                rawValue = .init(typeID)\n            default:\n                rawValue = rawContent.markdown.frontMatter[key]\n            }\n\n            properties[key] = try convert(\n                property: property,\n                rawValue: rawValue,\n                forKey: key,\n                slug: rawContent.origin.slug\n            )\n        }\n\n        // Convert schema-defined relations\n        var relations: [String: RelationValue] = [:]\n        for (key, relation) in contentType.relations.sorted(by: {\n            $0.key < $1.key\n        }) {\n            let rawValue = rawContent.markdown.frontMatter[key]\n            var identifiers: [String] = []\n\n            switch relation.type {\n            case .one:\n                if let id = rawValue?.value as? String {\n                    identifiers.append(id)\n                }\n            case .many:\n                if let ids = rawValue?.value as? [String] {\n                    identifiers.append(contentsOf: ids)\n                }\n            }\n\n            relations[key] = .init(\n                contentType: relation.references,\n                type: relation.type,\n                identifiers: identifiers\n            )\n        }\n\n        // Filter out reserved keys and schema-mapped fields to extract user-defined fields\n        let keysToRemove =\n            SystemPropertyKeys.allCases\n            .map { $0.rawValue }\n            + contentType.properties.keys\n            + contentType.relations.keys\n\n        var userDefined = rawContent.markdown.frontMatter\n        for key in keysToRemove {\n            userDefined.removeValue(forKey: key)\n        }\n\n        logger.trace(\n            \"Converting content\",\n            metadata: [\n                \"type\": .string(contentType.id),\n                \"typeAwareID\": .string(typeAwareID),\n                \"slug\": .string(slug),\n                \"origin\": .dictionary(\n                    [\n                        \"path\": .string(rawContent.origin.path.value),\n                        \"slug\": .string(rawContent.origin.slug),\n                    ]\n                ),\n            ]\n        )\n\n        return .init(\n            type: contentType,\n            typeAwareID: typeAwareID,\n            slug: .init(slug),\n            rawValue: rawContent,\n            properties: properties,\n            relations: relations,\n            userDefined: userDefined,\n            iteratorInfo: nil\n        )\n    }\n\n    // MARK: - filter\n\n    /// Applies the filtering rules to the provided content items.\n    ///\n    /// - Parameters:\n    ///   - filterRules: A dictionary mapping content type identifiers to filtering conditions.\n    ///   - contents: The list of `Content` items to filter.\n    ///   - now: The current timestamp used for time-based filtering.\n    /// - Returns: A new list containing only the filtered content items.\n    func apply(\n        filterRules: [String: Condition],\n        to contents: [Content],\n        now: TimeInterval\n    ) -> [Content] {\n        let groups = Dictionary(grouping: contents, by: { $0.type.id })\n\n        var result: [Content] = []\n        for (id, contents) in groups {\n            if let condition = filterRules[id] ?? filterRules[\"*\"] {\n                let items = contents.run(\n                    query: .init(\n                        contentType: id,\n                        filter: condition\n                    ),\n                    now: now,\n                    logger: logger\n                )\n                result.append(contentsOf: items)\n            }\n            else {\n                result.append(contentsOf: contents)\n            }\n        }\n        return result\n    }\n\n    // MARK: - iterators\n\n    func apply(\n        iterators: [String: Query],\n        to contents: [Content],\n        baseURL: String,\n        now: TimeInterval\n    ) -> [Content] {\n        var finalContents: [Content] = []\n\n        for content in contents {\n            if let iteratorID = content.slug.extractIteratorID() {\n                guard\n                    let query = iterators[iteratorID]\n                else {\n                    continue\n                }\n\n                let countQuery = Query(\n                    contentType: query.contentType,\n                    scope: query.scope,\n                    limit: nil,\n                    offset: nil,\n                    filter: query.filter,\n                    orderBy: query.orderBy\n                )\n\n                let total =\n                    contents.run(query: countQuery, now: now, logger: logger)\n                    .count\n                let limit = max(1, query.limit ?? 10)\n                let numberOfPages = (total + limit - 1) / limit\n\n                for i in 0..<numberOfPages {\n                    let offset = i * limit\n                    let currentPageIndex = i + 1\n\n                    var alteredContent = content\n                    rewrite(\n                        iteratorID: iteratorID,\n                        pageIndex: currentPageIndex,\n                        &alteredContent.typeAwareID\n                    )\n                    rewrite(\n                        iteratorID: iteratorID,\n                        pageIndex: currentPageIndex,\n                        &alteredContent.slug.value\n                    )\n                    rewrite(\n                        number: currentPageIndex,\n                        total: numberOfPages,\n                        &alteredContent.properties\n                    )\n                    rewrite(\n                        number: currentPageIndex,\n                        total: numberOfPages,\n                        &alteredContent.userDefined\n                    )\n\n                    if !alteredContent.rawValue.markdown.contents.isEmpty {\n                        alteredContent.rawValue.markdown.contents = replace(\n                            in: alteredContent.rawValue.markdown.contents,\n                            number: currentPageIndex,\n                            total: numberOfPages\n                        )\n                    }\n\n                    let links = (0..<numberOfPages)\n                        .map { i in\n                            let pageIndex = i + 1\n                            let permalink = content.slug.permalink(\n                                baseURL: baseURL\n                            )\n                            return IteratorInfo.Link(\n                                number: pageIndex,\n                                permalink: permalink.replacing(\n                                    [\"{{\\(iteratorID)}}\": String(pageIndex)]\n                                ),\n                                isCurrent: pageIndex == currentPageIndex\n                            )\n                        }\n\n                    let items = contents.run(\n                        query: .init(\n                            contentType: query.contentType,\n                            limit: limit,\n                            offset: offset,\n                            filter: query.filter,\n                            orderBy: query.orderBy\n                        ),\n                        now: now,\n                        logger: logger\n                    )\n\n                    alteredContent.iteratorInfo = .init(\n                        current: currentPageIndex,\n                        total: numberOfPages,\n                        limit: limit,\n                        items: items,\n                        links: links,\n                        scope: query.scope\n                    )\n\n                    finalContents.append(alteredContent)\n                }\n            }\n            else {\n                finalContents.append(content)\n            }\n        }\n        return finalContents\n    }\n\n    // MARK: - asset resolution\n\n    func apply(\n        assetProperties: [Pipeline.Assets.Property],\n        to contents: [Content],\n        contentsURL: URL,\n        assetsPath: String,\n        baseURL: String\n    ) throws -> [Content] {\n        var results: [Content] = []\n\n        for content in contents {\n            var item: Content = content\n\n            for property in assetProperties {\n                let path = item.rawValue.origin.path\n                let url = contentsURL.appendingPathComponent(path.value)\n                let assetsURL = url.appending(path: assetsPath)\n\n                let filteredAssets = filterFilePaths(\n                    from: content.rawValue.assets,\n                    input: property.input\n                )\n\n                guard !filteredAssets.isEmpty else {\n                    continue\n                }\n\n                let assetKeys =\n                    filteredAssets.compactMap {\n                        $0.split(separator: \".\").first\n                    }\n                    .map(String.init)\n\n                let resolvedAssets = filteredAssets.map {\n                    \"./\\(assetsPath)/\\($0)\"\n                        .resolveAsset(\n                            baseURL: baseURL,\n                            assetsPath: assetsPath,\n                            slug: content.slug.value\n                        )\n                }\n\n                let frontMatter = item.rawValue.markdown.frontMatter\n\n                let finalAssets =\n                    property.resolvePath ? resolvedAssets : filteredAssets\n\n                switch property.action {\n                case .add:\n                    if let originalItems = frontMatter[property.property]?\n                        .arrayValue(as: String.self)\n                    {\n                        item.properties[property.property] = .init(\n                            originalItems + finalAssets\n                        )\n                    }\n                    else {\n                        item.properties[property.property] = .init(finalAssets)\n                    }\n                case .set:\n                    if finalAssets.count == 1 {\n                        let asset = finalAssets[0]\n                        item.properties[property.property] = .init(asset)\n                    }\n                    else {\n                        item.properties[property.property] = .init(\n                            createDictionaryValues(\n                                assetKeys: assetKeys,\n                                array: finalAssets\n                            )\n                        )\n                    }\n                case .load:\n                    if filteredAssets.count == 1 {\n                        let asset = filteredAssets[0]\n                        let url = assetsURL.appending(path: asset)\n                        let contents = try String(\n                            contentsOf: url,\n                            encoding: .utf8\n                        )\n                        item.properties[property.property] = .init(contents)\n                    }\n                    else {\n                        var values: [String: AnyCodable] = [:]\n                        for i in 0..<filteredAssets.count {\n                            let asset = filteredAssets[i]\n                            let url = assetsURL.appending(path: asset)\n                            let contents = try String(\n                                contentsOf: url,\n                                encoding: .utf8\n                            )\n                            values[assetKeys[i]] = .init(contents)\n                        }\n                        item.properties[property.property] = .init(values)\n                    }\n                // TODO: check extension, add json support\n                case .parse:\n                    if filteredAssets.count == 1 {\n                        let asset = filteredAssets[0]\n                        let url = assetsURL.appending(path: asset)\n                        let data = try Data(contentsOf: url)\n                        let yaml = try ToucanYAMLDecoder()\n                            .decode(AnyCodable.self, from: data)\n                        item.properties[property.property] = yaml\n                    }\n                    else {\n                        var values: [String: AnyCodable] = [:]\n                        for i in 0..<filteredAssets.count {\n                            let asset = filteredAssets[i]\n                            let url = assetsURL.appending(path: asset)\n                            let data = try Data(contentsOf: url)\n                            let yaml = try ToucanYAMLDecoder()\n                                .decode(AnyCodable.self, from: data)\n                            values[assetKeys[i]] = yaml\n                        }\n                        item.properties[property.property] = .init(values)\n                    }\n                }\n            }\n            results.append(item)\n        }\n        return results\n    }\n\n    func applyBehaviors(\n        pipeline: Pipeline,\n        to contents: [Content],\n        contentsURL: URL,\n        assetsPath: String\n    ) throws -> [PipelineResult] {\n        var results: [PipelineResult] = []\n\n        for content in contents {\n            var assetsReady: Set<String> = .init()\n\n            for behavior in pipeline.assets.behaviors {\n                let isAllowed = pipeline.contentTypes.isAllowed(\n                    contentType: content.type.id\n                )\n                guard isAllowed else {\n                    continue\n                }\n                let remainingAssets = Set(content.rawValue.assets)\n                    .subtracting(assetsReady)\n\n                let matchingRemainingAssets = filterFilePaths(\n                    from: Array(remainingAssets),\n                    input: behavior.input\n                )\n\n                guard !matchingRemainingAssets.isEmpty else {\n                    continue\n                }\n\n                for inputAsset in matchingRemainingAssets {\n                    let basePath = content.rawValue.origin.path\n\n                    let sourcePath = [\n                        basePath.value,\n                        assetsPath,\n                        inputAsset,\n                    ]\n                    .joined(separator: \"/\")\n\n                    let file = getNameAndExtension(from: inputAsset)\n\n                    let destPath = [\n                        assetsPath,\n                        content.slug.value,\n                        inputAsset,\n                    ]\n                    .filter { !$0.isEmpty }\n                    .joined(separator: \"/\")\n                    .split(separator: \"/\")\n                    .dropLast()\n                    .joined(separator: \"/\")\n\n                    logger.trace(\n                        \"Resolving matching asset behavior.\",\n                        metadata: [\n                            \"behavior\": .string(behavior.id),\n                            \"source\": .string(sourcePath),\n                            \"destination\": .string(destPath),\n                        ]\n                    )\n\n                    let fileURL = contentsURL.appending(path: sourcePath)\n\n                    switch behavior.id {\n                    case CompileSASSBehavior.id:\n                        let script = try CompileSASSBehavior()\n                        let css = try script.run(fileURL: fileURL)\n\n                        // TODO: proper output management later on\n                        results.append(\n                            .init(\n                                source: .asset(css),\n                                destination: .init(\n                                    path: destPath,\n                                    file: behavior.output.name,\n                                    ext: behavior.output.ext\n                                )\n                            )\n                        )\n\n                    case MinifyCSSBehavior.id:\n                        let script = MinifyCSSBehavior()\n                        let css = try script.run(fileURL: fileURL)\n\n                        results.append(\n                            .init(\n                                source: .asset(css),\n                                destination: .init(\n                                    path: destPath,\n                                    file: behavior.output.name,\n                                    ext: behavior.output.ext\n                                )\n                            )\n                        )\n\n                    default:  // copy\n                        results.append(\n                            .init(\n                                source: .assetFile(sourcePath),\n                                destination: .init(\n                                    path: destPath,\n                                    file: file.name,\n                                    ext: file.ext\n                                )\n                            )\n                        )\n                    }\n\n                    assetsReady.insert(inputAsset)\n                }\n            }\n        }\n\n        return results\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSDK/Content/ContentTypeResolver.swift",
    "content": "//\n//  ContentTypeResolver.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 30..\n//\n\nimport ToucanCore\nimport ToucanSource\n\nenum ContentTypeResolverError: ToucanError {\n    case missingContentType(String, String)\n    case unknown(Error)\n\n    var underlyingErrors: [any Error] {\n        switch self {\n        case .missingContentType:\n            []\n        case let .unknown(error):\n            [error]\n        }\n    }\n\n    var logMessage: String {\n        switch self {\n        case let .missingContentType(id, path):\n            \"Missing content type for identifier: `\\(id)` at `\\(path)`.\"\n        case let .unknown(error):\n            error.localizedDescription\n        }\n    }\n\n    var userFriendlyMessage: String {\n        switch self {\n        case .missingContentType:\n            \"Missing content type.\"\n        case .unknown:\n            \"Unknown content conversion error.\"\n        }\n    }\n}\n\nstruct ContentTypeResolver {\n\n    let contentTypes: [ContentType]\n\n    init(\n        types: [ContentType],\n        pipelines: [Pipeline]\n    ) {\n        let virtualTypes = pipelines.compactMap {\n            $0.definesType ? ContentType(id: $0.id) : nil\n        }\n\n        self.contentTypes = (types + virtualTypes).sorted { $0.id < $1.id }\n    }\n\n    func getContentType(\n        for origin: Origin,\n        using id: String?\n    ) throws(ContentTypeResolverError) -> ContentType {\n        if let id {\n            guard\n                let result = contentTypes.first(where: { $0.id == id })\n            else {\n                throw .missingContentType(id, origin.path.value)\n            }\n            return result\n        }\n\n        if let type = contentTypes.first(\n            where: { type in\n                type.paths.contains { origin.path.value.hasPrefix($0) }\n            }\n        ) {\n            return type\n        }\n\n        let results = contentTypes.filter(\\.default)\n        precondition(\n            !results.isEmpty,\n            \"Don't forget to validate build target first.\"\n        )\n        return results[0]\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSDK/Content/IteratorInfo.swift",
    "content": "//\n//  IteratorInfo.swift\n//  Toucan\n//\n//  Created by gerp83 on 2025. 04. 17..\n//\n\n/// Provides pagination and iteration metadata for a content collection,\n/// used when rendering paginated list views.\npublic struct IteratorInfo {\n    /// Represents a navigation link within a paginated content sequence.\n    public struct Link: Codable {\n        /// The page number this link points to.\n        public var number: Int\n\n        /// The permalink URL for this page.\n        public var permalink: String\n\n        /// Whether this link refers to the currently active page.\n        public var isCurrent: Bool\n\n        /// Initializes a new pagination link.\n        ///\n        /// - Parameters:\n        ///   - number: The page number.\n        ///   - permalink: The URL for that page.\n        ///   - isCurrent: Whether this link is for the current page.\n        public init(\n            number: Int,\n            permalink: String,\n            isCurrent: Bool\n        ) {\n            self.number = number\n            self.permalink = permalink\n            self.isCurrent = isCurrent\n        }\n    }\n\n    /// The current page number (1-based).\n    public var current: Int\n\n    /// The total number of pages in the iterator.\n    public var total: Int\n\n    /// The number of items per page.\n    public var limit: Int\n\n    /// The subset of `Content` items that belong to the current page.\n    public var items: [Content]\n\n    /// A list of links to all available pages for UI navigation.\n    public var links: [Link]\n\n    /// An optional scope key used to identify the context or view this iterator belongs to.\n    ///\n    /// This can help differentiate between multiple iterators for the same content type\n    /// (e.g., \"allPosts\", \"featuredPosts\").\n    public var scope: String?\n\n    /// Initializes a new iterator metadata structure.\n    ///\n    /// - Parameters:\n    ///   - current: The current page number.\n    ///   - total: The total number of pages.\n    ///   - limit: Items per page.\n    ///   - items: The content items for the current page.\n    ///   - links: Pagination links to all pages.\n    ///   - scope: An optional scope identifier.\n    public init(\n        current: Int,\n        total: Int,\n        limit: Int,\n        items: [Content],\n        links: [Link],\n        scope: String?\n    ) {\n        self.current = current\n        self.total = total\n        self.limit = limit\n        self.items = items\n        self.links = links\n        self.scope = scope\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSDK/Content/Query+Resolve.swift",
    "content": "//\n//  Query+Resolve.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 16..\n//\n\nimport ToucanSource\n\npublic extension Condition {\n    /// Recursively resolves dynamic placeholders in the condition using a parameter map.\n    ///\n    /// Placeholders must be strings in the form `{{parameterKey}}` and will be\n    /// replaced by values from the given parameters dictionary.\n    ///\n    /// - Parameter parameters: A dictionary of key-value pairs to substitute into the condition.\n    /// - Returns: A new `Condition` with resolved values where applicable.\n    func resolve(with parameters: [String: AnyCodable]) -> Self {\n        switch self {\n        case let .field(key, op, value):\n            guard\n                let stringValue = value.value(as: String.self),\n                stringValue.count > 4,\n                stringValue.hasPrefix(\"{{\"),\n                stringValue.hasSuffix(\"}}\")\n            else {\n                return self\n            }\n\n            let paramKeyToUse = String(stringValue.dropFirst(2).dropLast(2))\n            guard let newValue = parameters[paramKeyToUse] else {\n                return self\n            }\n\n            return .field(key: key, operator: op, value: newValue)\n\n        case let .and(conditions):\n            return .and(conditions.map { $0.resolve(with: parameters) })\n\n        case let .or(conditions):\n            return .or(conditions.map { $0.resolve(with: parameters) })\n        }\n    }\n}\n\npublic extension Query {\n    /// Resolves dynamic filter parameters by injecting values into the filter condition tree.\n    ///\n    /// This is useful when filters include placeholders that need to be resolved at runtime.\n    ///\n    /// - Parameter parameters: A dictionary of key-value pairs to replace placeholders in the filter.\n    /// - Returns: A new `Query` instance with resolved filter conditions.\n    func resolveFilterParameters(\n        with parameters: [String: AnyCodable]\n    ) -> Self {\n        .init(\n            contentType: contentType,\n            scope: scope,\n            limit: limit,\n            offset: offset,\n            filter: filter?.resolve(with: parameters),\n            orderBy: orderBy\n        )\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSDK/Content/RelationValue.swift",
    "content": "//\n//  RelationValue.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 01. 30..\n//\n\nimport ToucanSource\n\n/// Represents the resolved value of a relation in content, including the target content type,\n/// the relation's cardinality, and the identifiers of related items.\npublic struct RelationValue {\n    /// The type of content this relation points to (e.g., `\"author\"`, `\"post\"`, `\"product\"`).\n    public var contentType: String\n\n    /// The relation type indicating if it's a one-to-one or one-to-many relationship.\n    public var type: RelationType\n\n    /// A list of string identifiers for the related content items.\n    /// For `.one`, this should typically contain a single ID; for `.many`, multiple.\n    public var identifiers: [String]\n\n    /// Initializes a new `RelationValue` representing the resolved target(s) of a content relation.\n    ///\n    /// - Parameters:\n    ///   - contentType: The name of the target content type.\n    ///   - type: The type of relation (single or multiple).\n    ///   - identifiers: A list of string IDs pointing to related content.\n    public init(\n        contentType: String,\n        type: RelationType,\n        identifiers: [String]\n    ) {\n        self.contentType = contentType\n        self.type = type\n        self.identifiers = identifiers\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSDK/DateFormats/DateContext.swift",
    "content": "//\n//  DateContext.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 02. 12..\n//\n\n/// A configuration container for date, time, and custom date-time formatting patterns.\n///\n/// `DateFormats` includes predefined formatting levels (full, long, medium, short)\n/// for both dates and times, as well as support for arbitrary format labels.\npublic struct DateContext: Codable {\n\n    /// Represents standardized formatting levels for a date or time value.\n    ///\n    /// These levels mirror common locale-aware date style options.\n    public struct Standard: Codable {\n        /// A fully verbose date format (e.g., `\"EEEE, MMMM d, yyyy\"`).\n        public var full: String\n\n        /// A long-form date format (e.g., `\"MMMM d, yyyy\"`).\n        public var long: String\n\n        /// A medium-form date format (e.g., `\"MMM d, yyyy\"`).\n        public var medium: String\n\n        /// A short-form date format (e.g., `\"M/d/yy\"`).\n        public var short: String\n\n        /// Initializes a new `Standard` date format set.\n        ///\n        /// - Parameters:\n        ///   - full: Full verbose date format string.\n        ///   - long: Long format string.\n        ///   - medium: Medium format string.\n        ///   - short: Short format string.\n        public init(\n            full: String,\n            long: String,\n            medium: String,\n            short: String\n        ) {\n            self.full = full\n            self.long = long\n            self.medium = medium\n            self.short = short\n        }\n    }\n\n    /// Standardized date format strings (e.g., full, medium, short).\n    public var date: Standard\n\n    /// Standardized time format strings (e.g., full, medium, short).\n    public var time: Standard\n\n    /// A standard iso8601 date string.\n    public var iso8601: String\n\n    /// A Unix timestamp representing a default or reference point in time.\n    public var timestamp: Double\n\n    /// Additional named date format strings keyed by label.\n    ///\n    /// These can be used for custom formatting beyond the standard levels.\n    public var formats: [String: String]\n\n    /// Initializes a `DateFormats` configuration.\n    ///\n    /// - Parameters:\n    ///   - date: Standardized date formatting options.\n    ///   - time: Standardized time formatting options.\n    ///   - timestamp: A base or reference timestamp, typically in Unix format.\n    ///   - iso8601: A standard iso8601 date string.\n    ///   - formats: Custom named format strings for specialized use cases.\n    public init(\n        date: Standard,\n        time: Standard,\n        timestamp: Double,\n        iso8601: String,\n        formats: [String: String]\n    ) {\n        self.date = date\n        self.time = time\n        self.timestamp = timestamp\n        self.iso8601 = iso8601\n        self.formats = formats\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSDK/DateFormats/ToucanDateFormatters.swift",
    "content": "//\n//  ToucanDateFormatters.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 26..\n//\n\nimport Foundation\nimport Logging\nimport ToucanCore\nimport ToucanSource\n\n/// ```\n/// target:\n///     dev:\n///        input: ./src\n///        output: ./dist\n///        config: ./src/config.dev.yml => auto lookup like this?\n///    -> default looks up for config.yml\n///\n///     live:\n///        config: ./src/config.live.yml\n///\n///    config.dev.yml:\n///        url: http://localhost:3000/\n///\n///        # output date formats basis\n///\n///        date:\n///           input:\n///              # input date formats basis\n///              locale: en-US\n///              timezone: Americas/Los_Angeles\n///              format: yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\n///           output:\n///              locale: en-US\n///              timezone: Americas/Los_Angeles\n///           formats:\n///              year:\n///                 format: \"y\"\n///                 locale: hu-HU\n///                 timezone: Europe/Budapest\n///\n///     pipeline -> overrides config completely\n///        date:\n///            input:\n///                locale: ???\n///                timezone: ???\n///                format: yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\n///            output:\n///                locale: en-US\n///                timezone: Americas/Los_Angeles\n///            formats:\n///               year:\n///                 format: \"y\"\n///                 locale: ???\n///                 timezone: ???\n///\n///    # content type\n///        post\n///            publication:\n///                type: date\n///                config: # input\n///                    format:\n///                    locale:\n///                    timeZone:\n/// ```\n\n/// Extension to configure `DateFormatter` with localization and config options.\nprivate extension DateFormatter {\n    /// Creates and configures a `DateFormatter`.\n    ///\n    /// - Parameters:\n    ///   - localization: The locale and time zone settings to apply.\n    ///   - block: A closure to further configure the formatter.\n    /// - Returns: A fully configured `DateFormatter`.\n    static func build(\n        using localization: DateLocalization = .defaults,\n        _ block: (inout DateFormatter) -> Void\n    ) -> DateFormatter {\n        var formatter = DateFormatter()\n        formatter.use(localization: localization)\n        formatter.dateStyle = .none\n        formatter.timeStyle = .none\n        block(&formatter)\n        return formatter\n    }\n\n    /// Applies the given localization (locale and time zone) to the formatter.\n    ///\n    /// - Parameter localization: The locale and time zone options.\n    func use(localization: DateLocalization) {\n        let id = Locale.identifier(.icu, from: localization.locale)\n        locale = .init(identifier: id)\n        timeZone = .init(identifier: localization.timeZone)\n    }\n\n    /// Applies a `DateFormatterConfig` (format, locale, time zone) to the formatter.\n    ///\n    /// - Parameter config: The date formatter configuration.\n    func use(config: DateFormatterConfig) {\n        use(localization: config.localization)\n        dateFormat = config.format\n    }\n}\n\n/// Holds system date and time style `DateFormatter` instances and an ISO8601 formatter.\nprivate struct SystemDateFormatters {\n\n    struct Date {\n        var full: DateFormatter\n        var long: DateFormatter\n        var medium: DateFormatter\n        var short: DateFormatter\n    }\n\n    struct Time {\n        var full: DateFormatter\n        var long: DateFormatter\n        var medium: DateFormatter\n        var short: DateFormatter\n    }\n\n    var date: Date\n    var time: Time\n    var iso8601: DateFormatter\n}\n\n/// Main utility for parsing and formatting dates based on project configuration.\n///\n/// Combines input parsing, system-style formatters, and user-defined formats.\npublic struct ToucanInputDateFormatter {\n\n    private var dateConfig: Config.DataTypes.Date\n    private var inputFormatter: DateFormatter\n    private var ephemeralFormatter: DateFormatter\n\n    var logger: Logger\n\n    /// Initializes the date formatter utility.\n    ///\n    /// - Parameters:\n    ///   - dateConfig: The base date configuration from the project.\n    ///   - logger: A logger instance for diagnostics.\n    public init(\n        dateConfig: Config.DataTypes.Date,\n        logger: Logger = .subsystem(\"input-date-formatter\")\n    ) {\n        self.dateConfig = dateConfig\n        self.inputFormatter = .build { $0.use(config: dateConfig.input) }\n        self.ephemeralFormatter = .build { $0.use(config: dateConfig.input) }\n        self.logger = logger\n    }\n\n    /// Parses a date string into a `Date` object.\n    ///\n    /// - Parameters:\n    ///   - string: The string representation of the date.\n    ///   - config: Optional `DateFormatterConfig` to override the input format.\n    /// - Returns: A `Date` if parsing succeeds, otherwise `nil`.\n    public func date(\n        from string: String,\n        using config: DateFormatterConfig? = nil\n    ) -> Date? {\n        if let config {\n            ephemeralFormatter.use(config: config)\n\n            return ephemeralFormatter.date(from: string)\n        }\n        return inputFormatter.date(from: string)\n    }\n\n    /// Converts a date into a `String` object.\n    ///\n    /// - Parameters:\n    ///   - date: The date representation.\n    ///   - config: Optional `DateFormatterConfig` to override the input format.\n    /// - Returns: A `String` using the provided date format config.\n    public func string(\n        from date: Date,\n        using config: DateFormatterConfig? = nil\n    ) -> String {\n        if let config {\n            ephemeralFormatter.use(config: config)\n\n            return ephemeralFormatter.string(from: date)\n        }\n        return inputFormatter.string(from: date)\n    }\n}\n\n/// Main utility for parsing and formatting dates based on project configuration.\n///\n/// Combines input parsing, system-style formatters, and user-defined formats.\npublic struct ToucanOutputDateFormatter {\n\n    private var dateConfig: Config.DataTypes.Date\n    private var pipelineDateConfig: Pipeline.DataTypes.Date?\n    private var systemFormatters: SystemDateFormatters\n    private var userFormatters: [String: DateFormatter]\n\n    var logger: Logger\n\n    /// Initializes the date formatter utility.\n    ///\n    /// - Parameters:\n    ///   - dateConfig: The base date configuration from the project.\n    ///   - pipelineDateConfig: Optional overrides for date configuration.\n    ///   - logger: A logger instance for diagnostics.\n    public init(\n        dateConfig: Config.DataTypes.Date,\n        pipelineDateConfig: Pipeline.DataTypes.Date? = nil,\n        logger: Logger = .subsystem(\"date-formatter\")\n    ) {\n        self.dateConfig = dateConfig\n        self.pipelineDateConfig = pipelineDateConfig\n\n        var localization = dateConfig.output\n        if let outputLocalization = pipelineDateConfig?.output {\n            localization = outputLocalization\n        }\n\n        self.systemFormatters = .init(\n            date: .init(\n                full: .build(using: localization) { $0.dateStyle = .full },\n                long: .build(using: localization) { $0.dateStyle = .long },\n                medium: .build(using: localization) { $0.dateStyle = .medium },\n                short: .build(using: localization) { $0.dateStyle = .short }\n            ),\n            time: .init(\n                full: .build(using: localization) { $0.timeStyle = .full },\n                long: .build(using: localization) { $0.timeStyle = .long },\n                medium: .build(using: localization) { $0.timeStyle = .medium },\n                short: .build(using: localization) { $0.timeStyle = .short }\n            ),\n            iso8601: .build(using: localization) {\n                $0.dateFormat = \"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\"\n            }\n        )\n        self.userFormatters = [:]\n        self.logger = logger\n\n        let userFormatterConfig = dateConfig.formats.merging(\n            (pipelineDateConfig?.formats ?? [:]),\n            uniquingKeysWith: { _, new in new }\n        )\n        for (key, config) in userFormatterConfig {\n            userFormatters[key] = .build(using: localization) {\n                $0.use(config: config)\n            }\n        }\n    }\n\n    /// Formats a `Date` into a `DateContext`, providing multiple style outputs and custom formats.\n    ///\n    /// - Parameter date: The `Date` to format.\n    /// - Returns: A `DateContext` containing formatted strings and timestamp.\n    public func format(\n        _ date: Date\n    ) -> DateContext {\n        .init(\n            date: .init(\n                full: systemFormatters.date.full.string(from: date),\n                long: systemFormatters.date.long.string(from: date),\n                medium: systemFormatters.date.medium.string(from: date),\n                short: systemFormatters.date.short.string(from: date)\n            ),\n            time: .init(\n                full: systemFormatters.time.full.string(from: date),\n                long: systemFormatters.time.long.string(from: date),\n                medium: systemFormatters.time.medium.string(from: date),\n                short: systemFormatters.time.short.string(from: date)\n            ),\n            timestamp: date.timeIntervalSince1970,\n            iso8601: systemFormatters.iso8601.string(from: date),\n            formats: userFormatters.mapValues { $0.string(from: date) }\n        )\n    }\n\n    /// Formats a time interval since 1970 into a `DateContext`.\n    ///\n    /// - Parameter timestamp: The time interval (seconds since 1970).\n    /// - Returns: A `DateContext` with formatted outputs.\n    public func format(\n        _ timestamp: TimeInterval\n    ) -> DateContext {\n        format(.init(timeIntervalSince1970: timestamp))\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSDK/Models/ContextBundle.swift",
    "content": "//\n//  ContextBundle.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 02. 21..\n//\n\nimport ToucanSource\n\n/// A bundle containing a single content item, its rendering context, and its destination metadata.\n///\n/// `ContextBundle` is typically used as an input for template rendering or output generation,\n/// combining the actual content with any supplemental data required for processing.\npublic struct ContextBundle {\n    /// The primary content item to be rendered or processed.\n    public var content: Content\n\n    /// A key-value store representing the extended rendering context (e.g., metadata, global variables).\n    /// These values can be used during template evaluation or logic processing.\n    public var context: [String: AnyCodable]\n\n    /// The intended destination of the output generated from this bundle.\n    public var destination: Destination\n\n    /// Initializes a new `ContextBundle` with content, context data, and a destination.\n    ///\n    /// - Parameters:\n    ///   - content: The `Content` instance to render.\n    ///   - context: A context dictionary providing additional rendering metadata or variables.\n    ///   - destination: Where the rendered output should be saved.\n    public init(\n        content: Content,\n        context: [String: AnyCodable],\n        destination: Destination\n    ) {\n        self.content = content\n        self.context = context\n        self.destination = destination\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSDK/Models/Destination.swift",
    "content": "//\n//  Destination.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 02. 21..\n//\n\n/// Represents the destination location and filename for rendered or transformed content.\npublic struct Destination: Sendable {\n    /// The relative or absolute path to the target directory where the file should be placed.\n    public var path: String\n\n    /// The base name of the file (without extension).\n    public var file: String\n\n    /// The file extension (e.g., \"html\", \"json\", \"md\").\n    public var ext: String\n\n    /// Initializes a new `Destination` describing where and how a file should be written.\n    ///\n    /// - Parameters:\n    ///   - path: The directory path to write the file to.\n    ///   - file: The base file name (without extension).\n    ///   - ext: The file extension (e.g., `\"html\"`, `\"json\"`).\n    public init(\n        path: String,\n        file: String,\n        ext: String\n    ) {\n        self.path = path\n        self.file = file\n        self.ext = ext\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSDK/Models/PipelineResult.swift",
    "content": "//\n//  PipelineResult.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 02. 21..\n//\n\n/// Represents the output of a content transformation pipeline, including the\n/// transformed content and its intended destination.\npublic struct PipelineResult: Sendable {\n    /// The source material for the pipeline result.\n    public enum Source: Sendable {\n        /// An asset source, that needs top be copied.\n        case assetFile(String)\n        /// A generated asset source\n        case asset(String)\n        /// The final transformed content (e.g., HTML, Markdown, etc.).\n        case content(String)\n\n        /// A Boolean value indicating whether the pipeline result's source is content-based.\n        /// Returns `true` if the source is `.content`, otherwise `false`.\n        public var isContent: Bool {\n            !isAsset\n        }\n\n        /// A Boolean value indicating whether the pipeline result's source is an asset.\n        /// Returns `true` if the source is `.asset`, otherwise `false`.\n        public var isAsset: Bool {\n            switch self {\n            case .content:\n                false\n            case .assetFile:\n                true\n            case .asset:\n                true\n            }\n        }\n    }\n\n    /// The source material.\n    public var source: Source\n\n    /// The destination metadata describing where or how the content should be output.\n    public var destination: Destination\n\n    /// Initializes a new `PipelineResult` with transformed content and a destination.\n    ///\n    /// - Parameters:\n    ///   - source: The source material.\n    ///   - destination: A `Destination` indicating where the result should be saved or rendered.\n    public init(\n        source: Source,\n        destination: Destination\n    ) {\n        self.source = source\n        self.destination = destination\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSDK/Models/Slug.swift",
    "content": "//\n//  Slug.swift\n//  Toucan\n//\n//  Created by gerp83 on 2025. 04. 17..\n//\n\n/// A value type representing a URL-friendly identifier for a content item.\npublic struct Slug: Equatable {\n\n    /// The raw slug string (e.g., `\"blog/welcome\"`, `\"about\"`, `\"\"`).\n    public var value: String\n\n    /// Initializes a new slug.\n    ///\n    /// - Parameter value: The raw slug string.\n    public init(\n        _ value: String\n    ) {\n        self.value = value\n    }\n\n    /// Extracts a dynamic iterator identifier from a slug value containing\n    /// a templated range (e.g., `\"blog/{{page}}\"` → `\"page\"`).\n    ///\n    /// - Returns: The identifier inside `{{...}}`, or `nil` if not found.\n    public func extractIteratorID() -> String? {\n        guard\n            let startRange = value.range(of: \"{{\"),\n            let endRange = value.range(\n                of: \"}}\",\n                range: startRange.upperBound..<value.endIndex\n            )\n        else {\n            return nil\n        }\n        return .init(value[startRange.upperBound..<endRange.lowerBound])\n    }\n\n    /// Constructs a permalink from the base URL and the slug.\n    ///\n    /// - Parameter baseURL: The base URL of the site (e.g., `\"https://example.com\"`).\n    /// - Returns: A fully-qualified permalink string (e.g., `\"https://example.com/blog/\"`).\n    public func permalink(baseURL: String) -> String {\n        let components = value.split(separator: \"/\").map(String.init)\n        if components.isEmpty {\n            return baseURL.ensureTrailingSlash()\n        }\n        if components.last?.split(separator: \".\").count ?? 0 > 1 {\n            // If last segment has a file extension, return without trailing slash\n            return ([baseURL] + components).joined(separator: \"/\")\n        }\n        return ([baseURL] + components)\n            .joined(separator: \"/\")\n            .ensureTrailingSlash()\n    }\n}\n\nextension Slug: Codable {\n\n    /// Generates a context-aware identifier string based on the last path component of a value.\n    ///\n    public func contextAwareIdentifier() -> String {\n        .init(value.split(separator: \"/\").last ?? \"\")\n    }\n\n    /// Creates a new instance by decoding from the given decoder.\n    ///\n    /// This initializer attempts to decode the value as a single string.\n    ///\n    /// - Parameter decoder: The decoder to read data from.\n    /// - Throws: An error if reading from the decoder fails, or if the data is not a single string.\n    public init(\n        from decoder: Decoder\n    ) throws {\n        let container = try decoder.singleValueContainer()\n        self.value = try container.decode(String.self)\n    }\n\n    /// Encodes this value into the given encoder.\n    ///\n    /// This method encodes the value as a single string.\n    ///\n    /// - Parameter encoder: The encoder to write data to.\n    /// - Throws: An error if encoding fails.\n    public func encode(\n        to encoder: Encoder\n    ) throws {\n        var container = encoder.singleValueContainer()\n        try container.encode(value)\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSDK/Outputs/ContextBundleToHTMLRenderer.swift",
    "content": "//\n//  ContextBundleToHTMLRenderer.swift\n//  Toucan\n//\n//  Created by Viasz-Kádi Ferenc on 2025. 05. 13..\n//\n\nimport Foundation\nimport Logging\nimport ToucanSource\n\nstruct ContextBundleToHTMLRenderer {\n\n    let mustacheRenderer: MustacheRenderer\n    let engineContentTypesOptions: [String: AnyCodable]\n    let pipelineViewKey: String\n    let logger: Logger\n\n    init(\n        pipeline: Pipeline,\n        templates: [String: String],\n        logger: Logger = .subsystem(\"context-bundle-to-html-renderer\")\n    ) throws {\n        self.mustacheRenderer = try MustacheRenderer(\n            templates: templates.mapValues {\n                try .init(string: $0)\n            }\n        )\n\n        let engineOptions = pipeline.engine.options\n        self.engineContentTypesOptions = engineOptions.dict(\"contentTypes\")\n        self.pipelineViewKey = [\n            ViewFrontMatterKeys.views.rawValue, pipeline.id,\n        ]\n        .joined(separator: \".\")\n        self.logger = logger\n    }\n\n    func render(\n        _ contextBundles: [ContextBundle]\n    ) -> [PipelineResult] {\n        contextBundles.compactMap { render($0) }\n    }\n\n    func render(\n        _ contextBundle: ContextBundle\n    ) -> PipelineResult? {\n        let contentTypeOptions = engineContentTypesOptions.dict(\n            contextBundle.content.type.id\n        )\n        let frontMatter = contextBundle.content.rawValue.markdown.frontMatter\n        let contentTypeView = contentTypeOptions.string(\n            ViewFrontMatterKeys.view.rawValue\n        )\n        let genericContentView = frontMatter.string(\n            ViewFrontMatterKeys.any.rawValue\n        )\n        let contentView = frontMatter.string(pipelineViewKey)\n        let viewId = contentView ?? genericContentView ?? contentTypeView\n\n        guard let viewId, !viewId.isEmpty else {\n            logger.warning(\n                \"No view has been specified for this content.\",\n                metadata: [\n                    \"slug\": \"\\(contextBundle.content.slug.value)\",\n                    \"type\": \"\\(contextBundle.content.type.id)\",\n                ]\n            )\n            return nil\n        }\n\n        let html = mustacheRenderer.render(\n            using: viewId,\n            with: contextBundle.context\n        )\n\n        guard let html else {\n            logger.warning(\n                \"Could not get valid HTML from content using view.\",\n                metadata: [\n                    \"slug\": .string(contextBundle.content.slug.value),\n                    \"type\": .string(contextBundle.content.type.id),\n                    \"view\": .string(viewId),\n                ]\n            )\n            return nil\n        }\n\n        return .init(\n            source: .content(html),\n            destination: contextBundle.destination\n        )\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSDK/Outputs/ContextBundleToJSONRenderer.swift",
    "content": "//\n//  ContextBundleToJSONRenderer.swift\n//  Toucan\n//\n//  Created by Viasz-Kádi Ferenc on 2025. 05. 13..\n//\n\nimport Foundation\nimport Logging\nimport ToucanSource\n\nstruct ContextBundleToJSONRenderer {\n    let pipeline: Pipeline\n    let encoder: JSONEncoder\n\n    let logger: Logger\n\n    let keyPath: String?\n    let keyPaths: [String: AnyCodable]?\n\n    init(\n        pipeline: Pipeline,\n        logger: Logger = .subsystem(\"context-bundle-to-json-renderer\")\n    ) {\n        let encoder = JSONEncoder()\n        encoder.outputFormatting = [\n            .prettyPrinted,\n            .withoutEscapingSlashes,\n            .sortedKeys,\n        ]\n\n        self.pipeline = pipeline\n        self.encoder = encoder\n        self.logger = logger\n\n        self.keyPath = pipeline.engine.options.string(\"keyPath\")\n        self.keyPaths = pipeline.engine.options.value(\n            \"keyPaths\",\n            as: [String: AnyCodable].self\n        )\n    }\n\n    private func data(\n        from context: [String: Any],\n        at keyPath: String?,\n        using encoder: JSONEncoder\n    ) throws -> Data? {\n        guard let keyPath else {\n            return nil\n        }\n\n        if let value = context.value(forKeyPath: keyPath) {\n            return try encoder.encode(AnyCodable(value))\n        }\n\n        return nil\n    }\n\n    private func data(\n        from context: [String: Any],\n        keyPaths: [String: AnyCodable]?,\n        using encoder: JSONEncoder\n    ) throws -> Data? {\n        var result: [String: AnyCodable] = [:]\n\n        guard let keyPaths else {\n            return nil\n        }\n\n        for (keyPath, value) in keyPaths {\n            guard let newKeyPath = value.value(as: String.self) else {\n                continue\n            }\n\n            if let value = context.value(forKeyPath: keyPath) {\n                result[newKeyPath] = .init(value)\n            }\n        }\n\n        return try encoder.encode(result)\n    }\n\n    func render(_ contextBundles: [ContextBundle]) -> [PipelineResult] {\n        contextBundles.compactMap {\n            render($0)\n        }\n    }\n\n    func render(_ contextBundle: ContextBundle) -> PipelineResult? {\n        let metadata: Logger.Metadata = [\n            \"slug\": \"\\(contextBundle.content.slug.value)\"\n        ]\n\n        let context = contextBundle.context\n        let unboxedContext = context.unboxed(encoder)\n\n        let encodedData = firstSucceeding([\n            {\n                try data(\n                    from: unboxedContext,\n                    keyPaths: keyPaths,\n                    using: encoder\n                )\n            },\n            { try data(from: unboxedContext, at: keyPath, using: encoder) },\n            { try encoder.encode(context) },\n        ])\n\n        guard let encodedData else {\n            logger.warning(\n                \"Could not encode context data as JSON object.\",\n                metadata: metadata\n            )\n            return nil\n        }\n\n        let json = String(data: encodedData, encoding: .utf8)\n\n        guard let json else {\n            logger.warning(\n                \"Could not encode context data as JSON output.\",\n                metadata: metadata\n            )\n            return nil\n        }\n        return .init(\n            source: .content(json),\n            destination: contextBundle.destination\n        )\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSDK/Renderers/BuildTargetSourceRenderer.swift",
    "content": "//\n//  BuildTargetSourceRenderer.swift\n//  Toucan\n//\n//  Created by Viasz-Kádi Ferenc on 2025. 03. 25..\n//\n\nimport Foundation\nimport Logging\nimport ToucanCore\nimport ToucanMarkdown\nimport ToucanSerialization\nimport ToucanSource\n\nenum BuildTargetSourceRendererError: ToucanError {\n    case invalidEngine(String)\n    case unknown(Error)\n\n    var underlyingErrors: [any Error] {\n        switch self {\n        case let .unknown(error):\n            [error]\n        default:\n            []\n        }\n    }\n\n    var logMessage: String {\n        switch self {\n        case let .invalidEngine(engine):\n            \"Invalid engine: `\\(engine)`.\"\n        case let .unknown(error):\n            error.localizedDescription\n        }\n    }\n\n    var userFriendlyMessage: String {\n        switch self {\n        case let .invalidEngine(engine):\n            \"Invalid engine: `\\(engine)`.\"\n        case .unknown:\n            \"Unknown source validator error.\"\n        }\n    }\n}\n\n/// Responsible for rendering the entire site bundle based on the `BuildTargetSource` configuration.\n///\n/// It processes content pipelines using the configured engine (Mustache, JSON, etc.),\n/// resolves content and site-level context, and outputs rendered content using templates\n/// or encoded formats.\npublic struct BuildTargetSourceRenderer {\n    /// Site configuration + all raw content\n    let buildTargetSource: BuildTargetSource\n    /// Generator metadata (e.g., version, name)\n    let generatorInfo: GeneratorInfo\n    /// Logger for warnings and errors\n    let logger: Logger\n    /// Cache\n    var contentContextCache: [String: [String: AnyCodable]] = [:]\n\n    /// Initializes a renderer from a source bundle.\n    ///\n    /// - Parameters:\n    ///   - buildTargetSource: The structured bundle containing settings, pipelines, and contents.\n    ///   - generatorInfo: Info about the content generator (defaults to `.current`).\n    ///   - logger: Logger for reporting issues or metrics.\n    public init(\n        buildTargetSource: BuildTargetSource,\n        generatorInfo: GeneratorInfo = .current,\n        logger: Logger = .subsystem(\"build-target-source-renderer\")\n    ) {\n        self.buildTargetSource = buildTargetSource\n        self.generatorInfo = generatorInfo\n        self.logger = logger\n    }\n\n    /// Returns the last content update based on the pipeline config\n    private func getLastContentUpdate(\n        contents: [Content],\n        pipeline: Pipeline,\n        now: TimeInterval\n    ) -> TimeInterval? {\n        var updateTypes = Set(contents.map(\\.type.id))\n        if !pipeline.contentTypes.lastUpdate.isEmpty {\n            updateTypes = updateTypes.filter {\n                pipeline.contentTypes.lastUpdate.contains($0)\n            }\n        }\n        let lastUpdate =\n            updateTypes.compactMap {\n                let items = contents.run(\n                    query: .init(\n                        contentType: $0,\n                        scope: nil,\n                        limit: 1,\n                        orderBy: [\n                            .init(\n                                key: SystemPropertyKeys.lastUpdate.rawValue,\n                                direction: .desc\n                            )\n                        ]\n                    ),\n                    now: now,\n                    logger: logger\n                )\n                return items.first?.rawValue.lastModificationDate\n            }\n            .sorted(by: >).first\n\n        logger.trace(\n            \"Last update for the pipeline.\",\n            metadata: [\n                \"pipeline\": .string(pipeline.id),\n                \"lastUpdate\": .string(\n                    Date(timeIntervalSince1970: lastUpdate ?? 0).description\n                ),\n            ]\n        )\n\n        return lastUpdate\n    }\n\n    private func baseURL() -> String {\n        buildTargetSource.target.url.dropTrailingSlash()\n    }\n\n    /// Returns the renderable context bundle for each content for a given pipeline using the global context\n    mutating func getContextBundles(\n        contents: [Content],\n        context globalContext: [String: AnyCodable],\n        pipeline: Pipeline,\n        dateFormatter: ToucanOutputDateFormatter,\n        now: TimeInterval\n    ) throws -> [ContextBundle] {\n        contents.compactMap { content in\n            let isAllowed = pipeline.contentTypes.isAllowed(\n                contentType: content.type.id\n            )\n            guard isAllowed else {\n                logger.trace(\n                    \"Skipping content type for the pipeline.\",\n                    metadata: [\n                        \"pipeline\": .string(pipeline.id),\n                        \"content-type\": .string(content.type.id),\n                        \"include\": .array(\n                            pipeline.contentTypes.include.map { .string($0) }\n                        ),\n                        \"exclude\": .array(\n                            pipeline.contentTypes.exclude.map { .string($0) }\n                        ),\n                    ]\n                )\n                return nil\n            }\n\n            let pipelineContext = getPipelineContext(\n                contents: contents,\n                pipeline: pipeline,\n                dateFormatter: dateFormatter,\n                now: now\n            )\n            .recursivelyMerged(with: globalContext)\n\n            return getContextBundle(\n                contents: contents,\n                content: content,\n                pipeline: pipeline,\n\n                pipelineContext: pipelineContext,\n                dateFormatter: dateFormatter,\n                now: now\n            )\n        }\n    }\n\n    mutating func getPipelineContext(\n        contents: [Content],\n        pipeline: Pipeline,\n        dateFormatter: ToucanOutputDateFormatter,\n        now: TimeInterval\n    ) -> [String: AnyCodable] {\n        var rawContext: [String: AnyCodable] = [:]\n        for (key, query) in pipeline.queries {\n            let results = contents.run(query: query, now: now, logger: logger)\n\n            rawContext[key] = .init(\n                results.map {\n                    getContentContext(\n                        contents: contents,\n                        for: $0,\n                        pipeline: pipeline,\n                        dateFormatter: dateFormatter,\n                        now: now,\n                        scopeKey: query.scope\n                            ?? Pipeline.Scope.Keys.list.rawValue\n                    )\n                }\n            )\n        }\n        return [\n            RootContextKeys.context.rawValue: .init(rawContext)\n        ]\n    }\n\n    mutating func getIteratorContext(\n        contents: [Content],\n        content: Content,\n        pipeline: Pipeline,\n        dateFormatter: ToucanOutputDateFormatter,\n        now: TimeInterval\n    ) -> [String: AnyCodable] {\n        guard let iteratorInfo = content.iteratorInfo else {\n            return [:]\n        }\n        let itemContext = iteratorInfo.items.map {\n            getContentContext(\n                contents: contents,\n                for: $0,\n                pipeline: pipeline,\n                dateFormatter: dateFormatter,\n                now: now,\n                scopeKey: iteratorInfo.scope\n                    ?? Pipeline.Scope.Keys.list.rawValue\n            )\n        }\n        return [\n            RootContextKeys.iterator.rawValue: .init(\n                [\n                    IteratorKeys.total.rawValue: .init(iteratorInfo.total),\n                    IteratorKeys.limit.rawValue: .init(iteratorInfo.limit),\n                    IteratorKeys.current.rawValue: .init(iteratorInfo.current),\n                    IteratorKeys.items.rawValue: .init(itemContext),\n                    IteratorKeys.links.rawValue: .init(iteratorInfo.links),\n                ] as [String: AnyCodable]\n            )\n        ]\n    }\n\n    mutating func getContextBundle(\n        contents: [Content],\n        content: Content,\n        pipeline: Pipeline,\n        pipelineContext: [String: AnyCodable],\n        dateFormatter: ToucanOutputDateFormatter,\n        now: TimeInterval\n    ) -> ContextBundle {\n        let pageContext = getContentContext(\n            contents: contents,\n            for: content,\n            pipeline: pipeline,\n            dateFormatter: dateFormatter,\n            now: now,\n            scopeKey: Pipeline.Scope.Keys.detail.rawValue\n        )\n\n        let iteratorContext = getIteratorContext(\n            contents: contents,\n            content: content,\n            pipeline: pipeline,\n            dateFormatter: dateFormatter,\n            now: now\n        )\n\n        let context: [String: AnyCodable] = [\n            RootContextKeys.page.rawValue: .init(pageContext)\n        ]\n        .recursivelyMerged(with: iteratorContext)\n        .recursivelyMerged(with: pipelineContext)\n\n        var outputArgs: [String: String] = [\n            \"{{id}}\": content.typeAwareID,\n            \"{{slug}}\": content.slug.value,\n        ]\n\n        if let info = content.iteratorInfo {\n            outputArgs[\"{{iterator.current}}\"] = String(info.current)\n            outputArgs[\"{{iterator.total}}\"] = String(info.total)\n            outputArgs[\"{{iterator.limit}}\"] = String(info.limit)\n        }\n\n        let path = pipeline.output.path.replacing(outputArgs)\n        let file = pipeline.output.file.replacing(outputArgs)\n        let ext = pipeline.output.ext.replacing(outputArgs)\n\n        return .init(\n            content: content,\n            context: context,\n            destination: .init(\n                path: path,\n                file: file,\n                ext: ext\n            )\n        )\n    }\n\n    mutating func getContentContext(\n        contents: [Content],\n        for content: Content,\n        pipeline: Pipeline,\n        dateFormatter: ToucanOutputDateFormatter,\n        now: TimeInterval,\n        scopeKey: String,\n        allowSubQueries: Bool = true  // allow top level queries only\n    ) -> [String: AnyCodable] {\n        var result: [String: AnyCodable] = [:]\n\n        let scope = pipeline.getScope(\n            keyedBy: scopeKey,\n            for: content.type.id\n        )\n\n        logger.trace(\n            \"Using scope for content\",\n            metadata: [\n                \"pipeline\": .string(pipeline.id),\n                \"content\": .string(content.slug.value),\n                \"scope\": .string(scopeKey),\n                \"context\": .array(\n                    scope.context.stringValues.map { .string($0) }\n                ),\n                \"fields\": .array(\n                    scope.fields.map { .string($0) }\n                ),\n                \"allowSubQueries\": .string(allowSubQueries.description),\n            ]\n        )\n\n        let cacheKey = [\n            pipeline.id,\n            content.slug.value,\n            scopeKey,\n            String(allowSubQueries),\n        ]\n        .joined(separator: \"_\")\n\n        if let cachedContext = contentContextCache[cacheKey] {\n            return cachedContext\n        }\n\n        if scope.context.contains(.userDefined) {\n            result = result.recursivelyMerged(with: content.userDefined)\n        }\n\n        if scope.context.contains(.properties) {\n            for (k, v) in content.properties {\n                if let p = content.type.properties[k] {\n                    switch p.type {\n                    /// resolve assets\n                    case .asset:\n                        guard let rawValue = v.stringValue() else {\n                            continue\n                        }\n\n                        let resolvedValue = rawValue.resolveAsset(\n                            baseURL: baseURL(),\n                            assetsPath: content.rawValue.assetsPath,\n                            slug: content.slug.value\n                        )\n\n                        result[k] = .init(resolvedValue)\n                    /// format dates\n                    case .date:\n                        guard let rawValue = v.doubleValue() else {\n                            continue\n                        }\n                        result[k] = .init(\n                            dateFormatter.format(rawValue)\n                        )\n                    default:\n                        result[k] = .init(v.value)\n                    }\n                }\n                else {\n                    result[k] = .init(v.value)\n                }\n            }\n\n            result[SystemPropertyKeys.slug.rawValue] = .init(content.slug.value)\n            result[SystemPropertyKeys.lastUpdate.rawValue] = .init(\n                dateFormatter.format(content.rawValue.lastModificationDate)\n            )\n            result[PageContextKeys.permalink.rawValue] = .init(\n                content.slug.permalink(baseURL: baseURL())\n            )\n        }\n\n        if scope.context.contains(.contents) {\n            let transformers = pipeline.transformers[\n                content.type.id\n            ]\n            let renderer = MarkdownRenderer(\n                configuration: .init(\n                    markdown: .init(\n                        customBlockDirectives: buildTargetSource.blocks\n                            .map {\n                                .init(\n                                    name: $0.name,\n                                    parameters: $0.parameters?\n                                        .map {\n                                            .init(\n                                                label: $0.label,\n                                                isRequired: $0.isRequired,\n                                                defaultValue: $0.defaultValue\n                                            )\n                                        },\n                                    requiresParentDirective: $0\n                                        .requiresParentDirective,\n                                    removesChildParagraph: $0\n                                        .removesChildParagraph,\n                                    tag: $0.tag,\n                                    attributes: $0.attributes?\n                                        .map {\n                                            .init(\n                                                name: $0.name,\n                                                value: $0.value\n                                            )\n                                        },\n                                    output: $0.output\n                                )\n                            }\n                    ),\n                    outline: .init(\n                        levels: buildTargetSource.config.renderer\n                            .outlineLevels\n                    ),\n                    readingTime: .init(\n                        wordsPerMinute: buildTargetSource.config\n                            .renderer.wordsPerMinute\n                    ),\n                    transformerPipeline: transformers.map {\n                        .init(\n                            run: $0.run.map {\n                                .init(path: $0.path, name: $0.name)\n                            },\n                            isMarkdownResult: $0.isMarkdownResult\n                        )\n                    },\n                    paragraphStyles: buildTargetSource.config.renderer\n                        .paragraphStyles.styles\n                )\n            )\n\n            let contents = renderer.render(\n                content: content.rawValue.markdown.contents,\n                typeAwareID: content.typeAwareID,\n                slug: content.slug.value,\n                assetsPath: buildTargetSource.config.contents.assets.path,\n                baseURL: baseURL()\n            )\n\n            result[PageContextKeys.contents.rawValue] = [\n                PageContentsKeys.html.rawValue: contents.html,\n                PageContentsKeys.readingTime.rawValue: contents.readingTime,\n                PageContentsKeys.outline.rawValue: contents.outline,\n            ]\n        }\n\n        if scope.context.contains(.relations) {\n            for (key, relation) in content.type.relations {\n                var orderBy: [Order] = []\n                if let order = relation.order {\n                    orderBy.append(order)\n                }\n\n                let relationContents = contents.run(\n                    query: .init(\n                        contentType: relation.references,\n                        filter: .field(\n                            key: \"id\",\n                            operator: .in,\n                            value: .init(\n                                content.relations[key]?.identifiers ?? []\n                            )\n                        ),\n                        orderBy: orderBy\n                    ),\n                    now: now,\n                    logger: logger\n                )\n\n                let relationContexts = relationContents.map {\n                    getContentContext(\n                        contents: contents,\n                        for: $0,\n                        pipeline: pipeline,\n                        dateFormatter: dateFormatter,\n                        now: now,\n                        scopeKey: Pipeline.Scope.Keys.reference.rawValue,\n                        allowSubQueries: false\n                    )\n                }\n\n                switch relation.type {\n                case .many:\n                    result[key] = .init(relationContexts)\n                case .one:\n                    if let item = relationContexts.first {\n                        result[key] = .init(item)\n                    }\n                }\n            }\n        }\n\n        if allowSubQueries, scope.context.contains(.queries) {\n            for (key, query) in content.type.queries {\n                let queryContents = contents.run(\n                    query: query.resolveFilterParameters(\n                        with: content.queryFields\n                    ),\n                    now: now,\n                    logger: logger\n                )\n\n                result[key] = .init(\n                    queryContents.map {\n                        getContentContext(\n                            contents: contents,\n                            for: $0,\n                            pipeline: pipeline,\n                            dateFormatter: dateFormatter,\n                            now: now,\n                            scopeKey: query.scope\n                                ?? Pipeline.Scope.Keys.list.rawValue,\n                            allowSubQueries: false\n                        )\n                    }\n                )\n            }\n        }\n\n        logger.trace(\n            \"Returning context for content\",\n            metadata: [\n                \"pipeline\": .string(pipeline.id),\n                \"content\": .string(content.slug.value),\n                \"scope\": .string(scopeKey),\n                \"context\": .array(\n                    scope.context.stringValues.map { .string($0) }\n                ),\n                \"fields\": .array(\n                    scope.fields.map { .string($0) }\n                ),\n                \"allowSubQueries\": .string(allowSubQueries.description),\n                \"result\": .dictionary(\n                    [\n                        \"slug\": .string(\n                            result[\"slug\"]?.stringValue() ?? \"nil\"\n                        ),\n                        \"permalink\": .string(\n                            result[PageContextKeys.permalink.rawValue]?\n                                .stringValue() ?? \"nil\"\n                        ),\n                    ]\n                ),\n            ]\n        )\n\n        guard !scope.fields.isEmpty else {\n            contentContextCache[cacheKey] = result\n            return result\n        }\n        contentContextCache[cacheKey] = result\n        return result.filter { scope.fields.contains($0.key) }\n    }\n\n    /// Renders pipelines by processing content using defined resolvers and formatting logic,\n    /// and executes a renderer block with the resulting context bundles.\n    ///\n    /// - Parameters:\n    ///   - now: The current date used for time-sensitive filtering and formatting.\n    ///   - rendererBlock: A closure that takes a pipeline and its corresponding context bundles,\n    ///     and returns the rendered pipeline results.\n    /// - Returns: An array of `PipelineResult` containing all results from all processed pipelines.\n    /// - Throws: Rethrows any error encountered during content processing or rendering.\n    public mutating func render(\n        now: Date,\n        rendererBlock:\n            @escaping (\n                (_: Pipeline, _: [ContextBundle]) throws -> [PipelineResult]\n            )\n    ) throws -> [PipelineResult] {\n        let now = now.timeIntervalSince1970\n\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n\n        let inputDateFormatter = ToucanInputDateFormatter(\n            dateConfig: buildTargetSource.config.dataTypes.date\n        )\n\n        // TODO: This should be in a .toucaninfo file or similar\n        let globalContext: [String: AnyCodable] = [\n            GlobalContextKeys.baseUrl.rawValue: .init(baseURL()),\n            GlobalContextKeys.generator.rawValue: .init(generatorInfo),\n        ]\n\n        let contentTypeResolver = ContentTypeResolver(\n            types: buildTargetSource.types,\n            pipelines: buildTargetSource.pipelines\n        )\n\n        let contentResolver = ContentResolver(\n            contentTypeResolver: contentTypeResolver,\n            encoder: encoder,\n            decoder: decoder,\n            dateFormatter: inputDateFormatter\n        )\n\n        let baseContents = try contentResolver.convert(\n            rawContents: buildTargetSource.rawContents\n        )\n\n        var results: [PipelineResult] = []\n\n        // TODO: `for` probably should happen in Toucan.swift, and we could deal with a single pipeline here\n        for pipeline in buildTargetSource.pipelines {\n            let filteredContents = contentResolver.apply(\n                filterRules: pipeline.contentTypes.filterRules,\n                to: baseContents,\n                now: now\n            )\n            let iteratedContents = contentResolver.apply(\n                iterators: pipeline.iterators,\n                to: filteredContents,\n                baseURL: baseURL(),\n                now: now\n            )\n\n            let finalContents = try contentResolver.apply(\n                assetProperties: pipeline.assets.properties,\n                to: iteratedContents,\n                contentsURL: buildTargetSource.locations.contentsURL,\n                assetsPath: buildTargetSource.config.contents.assets.path,\n                baseURL: baseURL()\n            )\n\n            let dateFormatter = ToucanOutputDateFormatter(\n                dateConfig: buildTargetSource.config.dataTypes.date,\n                pipelineDateConfig: pipeline.dataTypes.date\n            )\n\n            let assetResults = try contentResolver.applyBehaviors(\n                pipeline: pipeline,\n                to: finalContents,\n                contentsURL: buildTargetSource.locations.contentsURL,\n                assetsPath: buildTargetSource.config.contents.assets.path\n            )\n\n            results.append(contentsOf: assetResults)\n\n            let lastUpdate =\n                getLastContentUpdate(\n                    contents: finalContents,\n                    pipeline: pipeline,\n                    now: now\n                ) ?? now\n\n            let contextBundles = try getContextBundles(\n                contents: finalContents,\n                context: globalContext.recursivelyMerged(\n                    with: [\n                        SystemPropertyKeys.lastUpdate.rawValue: .init(\n                            dateFormatter.format(lastUpdate)\n                        ),\n                        GlobalContextKeys.generation.rawValue: .init(\n                            dateFormatter.format(now)\n                        ),\n                        GlobalContextKeys.site.rawValue: .init(\n                            buildTargetSource.settings.values\n                        ),\n                    ]\n                ),\n                pipeline: pipeline,\n                dateFormatter: dateFormatter,\n                now: now\n            )\n\n            logger.trace(\n                \"Rendering contents for pipeline\",\n                metadata: [\n                    \"pipeline\": .string(pipeline.id),\n                    \"counts\": [\n                        \"base\": .string(baseContents.count.description),\n                        \"iterated\": .string(iteratedContents.count.description),\n                        \"final\": .string(finalContents.count.description),\n                        \"context\": .string(contextBundles.count.description),\n                    ],\n                    \"final\": .array(\n                        finalContents.map(\\.slug.value).map { .string($0) }\n                    ),\n                    \"context\": .array(\n                        contextBundles.map(\n                            \\.content.slug.value\n                        )\n                        .map { .string($0) }\n                    ),\n                ]\n            )\n\n            results += try rendererBlock(pipeline, contextBundles)\n        }\n        return results\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSDK/Renderers/MustacheRenderer.swift",
    "content": "//\n//  MustacheRenderer.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 02. 16..\n//\n\nimport Foundation\nimport Logging\nimport Mustache\nimport ToucanSource\nimport ToucanCore\n\n/// Renders Mustache templates using a predefined template library and a dynamic context object.\npublic struct MustacheRenderer {\n\n    /// A list of all available template IDs in the library.\n    var ids: [String]\n\n    /// The Mustache template library holding precompiled templates.\n    var library: MustacheLibrary\n\n    /// Logger used for reporting missing templates or rendering failures.\n    var logger: Logger\n\n    /// Initializes a renderer with a set of compiled Mustache templates and a logger.\n    ///\n    /// - Parameters:\n    ///   - templates: A dictionary of template IDs and their corresponding `MustacheTemplate` objects.\n    ///   - logger: A logger instance used for error reporting.\n    public init(\n        templates: [String: MustacheTemplate],\n        logger: Logger = .subsystem(\"mustache-renderer\")\n    ) {\n        self.ids = Array(templates.keys)\n        self.library = .init(templates: templates)\n        self.logger = logger\n    }\n\n    /// Renders a Mustache template using the given context object.\n    ///\n    /// - Parameters:\n    ///   - id: The ID of the template to render.\n    ///   - object: A dictionary representing the context (`[String: AnyCodable]`).\n    /// - Returns: The rendered HTML string, or `nil` if rendering fails or the template is missing.\n    public func render(\n        using id: String,\n        with object: [String: AnyCodable]\n    ) -> String? {\n        // Ensure the ID is valid\n        guard ids.contains(id) else {\n            logger.error(\n                \"Missing or invalid template file.\",\n                metadata: [\n                    \"id\": .string(id)\n                ]\n            )\n            return nil\n        }\n\n        // Unwrap the object for rendering\n        let local = unwrap(object) as Any\n\n        // Attempt rendering using the Mustache library\n        guard let html = library.render(local, withTemplate: id) else {\n            logger.error(\n                \"Could not render HTML using the template file.\",\n                metadata: [\n                    \"id\": .string(id)\n                ]\n            )\n            return nil\n        }\n        return html\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSDK/Toucan.swift",
    "content": "//\n//  Toucan.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 04. 17..\n//\n\nimport FileManagerKit\nimport Foundation\nimport Logging\nimport ToucanCore\nimport ToucanSerialization\nimport ToucanSource\n\n/// Primary entry point for generating a static site using the Toucan framework.\npublic struct Toucan {\n\n    let fileManager: FileManagerKit\n    let encoder: ToucanEncoder\n    let decoder: ToucanDecoder\n    let logger: Logger\n\n    /// Initialize a new instance.\n    ///\n    /// - Parameters:\n    ///   - fileManager: The file manager used to perform file operations.\n    ///   - encoder: The encoder used to encode data. Defaults to a YAML encoder.\n    ///   - decoder: The decoder used to decode data. Defaults to a YAML decoder.\n    ///   - logger: A logger instance for logging. Defaults to a logger labeled \"toucan\".\n    public init(\n        fileManager: FileManagerKit = FileManager.default,\n        encoder: ToucanEncoder = ToucanYAMLEncoder(),\n        decoder: ToucanDecoder = ToucanYAMLDecoder(),\n        logger: Logger = .subsystem()\n    ) {\n        self.fileManager = fileManager\n        self.encoder = encoder\n        self.decoder = decoder\n        self.logger = logger\n    }\n\n    func resolveHomeURL(\n        for path: String\n    ) -> URL {\n        let home = fileManager.homeDirectoryForCurrentUser.path\n        return\n            .init(\n                fileURLWithPath: path.replacing([\"~\": home])\n            )\n            .standardized\n    }\n\n    func absoluteURL(\n        for path: String,\n        cwd url: URL? = nil\n    ) -> URL {\n        if path.hasPrefix(\"/\") {\n            return URL(filePath: path).standardized\n        }\n        if path.hasPrefix(\"~\") {\n            return resolveHomeURL(for: path).standardized\n        }\n        let cwdURL = url ?? URL(filePath: fileManager.currentDirectoryPath)\n        if path == \".\" || path == \"./\" {\n            return cwdURL.standardized\n        }\n        return cwdURL.appendingPathIfPresent(path).standardized\n    }\n\n    func resetDirectory(at url: URL) throws {\n        if fileManager.exists(at: url) {\n            try fileManager.delete(at: url)\n        }\n        try fileManager.createDirectory(\n            at: url,\n            attributes: nil\n        )\n    }\n\n    func prepareTemporaryWorkingDirectory() throws -> URL {\n        let url = fileManager\n            .temporaryDirectory\n            .appendingPathComponent(\"toucan\")\n            .appendingPathComponent(UUID().uuidString)\n\n        try resetDirectory(at: url)\n\n        logger.debug(\n            \"Working at temporary directory.\",\n            metadata: [\n                \"path\": .string(url.path())\n            ]\n        )\n\n        return url\n    }\n\n    func loadTargetConfig(\n        workDirURL: URL\n    ) throws -> TargetConfig {\n        try ObjectLoader(\n            url: workDirURL,\n            locations:\n                fileManager\n                .find(\n                    name: \"toucan\",\n                    extensions: [\"yml\", \"yaml\"],\n                    at: workDirURL\n                ),\n            encoder: encoder,\n            decoder: decoder\n        )\n        .load(TargetConfig.self)\n    }\n\n    func getActiveBuildTargets(\n        targetConfig: TargetConfig,\n        targetsToBuild: [String]\n    ) -> [Target] {\n        var buildTargets = targetConfig.targets.filter {\n            targetsToBuild.contains($0.name)\n        }\n        if buildTargets.isEmpty {\n            buildTargets.append(targetConfig.default)\n        }\n        return buildTargets\n    }\n\n    // MARK: - api\n\n    /// Generates the static site.\n    ///\n    /// - Parameters:\n    ///   - workDir: The working directory URL as a path string.\n    ///   - targetsToBuild: The list of target names to build.\n    ///   - now: The current date used during the build.\n    /// - Throws: An error if the generation fails.\n    public func generate(\n        workDir: String,\n        targetsToBuild: [String] = [],\n        now: Date = .init()\n    ) throws {\n        let workDirURL = absoluteURL(for: workDir)\n        let temporaryWorkDirURL = try prepareTemporaryWorkingDirectory()\n\n        do {\n            let targetConfig = try loadTargetConfig(\n                workDirURL: workDirURL\n            )\n            let activeBuildTargets = getActiveBuildTargets(\n                targetConfig: targetConfig,\n                targetsToBuild: targetsToBuild\n            )\n\n            for target in activeBuildTargets {\n                let sourceURL = absoluteURL(\n                    for: target.input,\n                    cwd: workDirURL\n                )\n                let distURL = absoluteURL(\n                    for: target.output,\n                    cwd: workDirURL\n                )\n\n                logger.debug(\n                    \"Building target.\",\n                    metadata: [\n                        \"name\": .string(target.name),\n                        \"input\": .string(target.input),\n                        \"output\": .string(target.output),\n                        \"workDir\": .string(workDirURL.path()),\n                        \"srcDir\": .string(sourceURL.path()),\n                        \"distDir\": .string(distURL.path()),\n                        \"tmpDir\": .string(temporaryWorkDirURL.path()),\n                    ]\n                )\n\n                let buildTargetSourceLoader = BuildTargetSourceLoader(\n                    sourceURL: workDirURL,\n                    target: target,\n                    fileManager: fileManager,\n                    encoder: encoder,\n                    decoder: decoder\n                )\n\n                let buildTargetSource = try buildTargetSourceLoader.load()\n\n                let validator = BuildTargetSourceValidator(\n                    buildTargetSource: buildTargetSource\n                )\n                try validator.validate()\n\n                let generatorInfo = GeneratorInfo.current\n                var renderer = BuildTargetSourceRenderer(\n                    buildTargetSource: buildTargetSource,\n                    generatorInfo: generatorInfo\n                )\n\n                let templateLoader = TemplateLoader(\n                    locations: buildTargetSource.locations,\n\n                    fileManager: fileManager,\n                    encoder: encoder,\n                    decoder: decoder\n                )\n\n                let results = try renderer.render(\n                    now: now\n                ) { pipeline, contextBundles in\n\n                    logger.trace(\n                        \"Rendering pipeline\",\n                        metadata: [\n                            \"id\": .string(pipeline.id),\n                            \"contextBundleCount\": .string(\n                                String(contextBundles.count)\n                            ),\n                        ]\n                    )\n\n                    switch pipeline.engine.id {\n                    case \"json\":\n                        let renderer = ContextBundleToJSONRenderer(\n                            pipeline: pipeline\n                        )\n                        return renderer.render(contextBundles)\n                    case \"mustache\":\n                        let template = try templateLoader.load()\n\n                        let templateValidator = try TemplateValidator(\n                            generatorInfo: generatorInfo\n                        )\n                        try templateValidator.validate(template)\n\n                        let renderer = try ContextBundleToHTMLRenderer(\n                            pipeline: pipeline,\n                            templates: template.getViewIDsWithContents()\n                        )\n                        return renderer.render(contextBundles)\n                    default:\n                        throw BuildTargetSourceRendererError.invalidEngine(\n                            pipeline.engine.id\n                        )\n                    }\n                }\n\n                logger.debug(\n                    \"Target ready.\",\n                    metadata: [\n                        \"name\": .string(target.name),\n                        \"resultsCount\": .string(String(results.count)),\n                    ]\n                )\n\n                try resetDirectory(at: temporaryWorkDirURL)\n\n                // MARK: - Copy default assets\n\n                let copyManager = CopyManager(\n                    fileManager: fileManager,\n                    sources: [\n                        buildTargetSource.locations.currentTemplateAssetsURL,\n                        buildTargetSource.locations\n                            .currentTemplateAssetOverridesURL,\n                        buildTargetSource.locations.siteAssetsURL,\n                    ],\n                    destination: temporaryWorkDirURL\n                )\n                try copyManager.copy()\n\n                // MARK: - Writing results\n\n                for result in results {\n                    let destinationFolder =\n                        temporaryWorkDirURL\n                        .appendingPathIfPresent(result.destination.path)\n\n                    try fileManager.createDirectory(\n                        at: destinationFolder,\n                        attributes: nil\n                    )\n\n                    let resultOutputURL =\n                        destinationFolder\n                        .appendingPathIfPresent(result.destination.file)\n                        .appendingPathExtension(result.destination.ext)\n\n                    switch result.source {\n                    case let .assetFile(path):\n                        let srcURL = buildTargetSource.locations.contentsURL\n                            .appendingPathIfPresent(path)\n                        try fileManager.copy(from: srcURL, to: resultOutputURL)\n                    case let .asset(string), let .content(string):\n                        try string.write(\n                            to: resultOutputURL,\n                            atomically: true,\n                            encoding: .utf8\n                        )\n                    }\n                }\n\n                // MARK: - Finalize and cleanup\n\n                try resetDirectory(at: distURL)\n                try fileManager.copyRecursively(\n                    from: temporaryWorkDirURL,\n                    to: distURL\n                )\n                try fileManager.delete(at: temporaryWorkDirURL)\n            }\n        }\n        catch {\n            try fileManager.delete(at: temporaryWorkDirURL)\n            throw error\n        }\n    }\n\n    /// Attempts to generate the static site and logs any errors encountered.\n    /// - Parameters:\n    ///   - workDir: The working directory URL as a path string.\n    ///   - targetsToBuild: The list of target names to build.\n    ///   - now: The current date used during the build.\n    /// - Returns: `true` if generation succeeds without errors; otherwise, `false`.\n    ///\n    @discardableResult\n    public func generateAndLogErrors(\n        workDir: String,\n        targetsToBuild: [String],\n        now: Date\n    ) -> Bool {\n        do {\n            try generate(\n                workDir: workDir,\n                targetsToBuild: targetsToBuild,\n                now: now\n            )\n            return true\n        }\n        catch let error as ToucanError {\n            logger.error(\"\\(error.logMessageStack())\")\n        }\n        catch {\n            logger.error(\"\\(error)\")\n        }\n        return false\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSDK/Utilities/Any+AnyCodable.swift",
    "content": "//\n//  Any+AnyCodable.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 03. 06..\n//\n\nimport ToucanSource\n\n/// Recursively unwraps a value that may contain `AnyCodable` types into native Swift types.\n///\n/// - Parameter value: A possibly wrapped `Any?`, including `[String: AnyCodable]`, `[AnyCodable]`, etc.\n/// - Returns: A fully unwrapped `Any?`, preserving dictionaries and arrays but removing all `AnyCodable` wrappers.\npublic func unwrap(_ value: Any?) -> Any? {\n    if let anyCodable = value as? AnyCodable {\n        return unwrap(anyCodable.value)\n    }\n    if let dict = value as? [String: AnyCodable] {\n        var result: [String: Any] = [:]\n        for (key, val) in dict {\n            result[key] = unwrap(val)\n        }\n        return result\n    }\n    if let dict = value as? [String: Any] {\n        var result: [String: Any] = [:]\n        for (key, val) in dict {\n            result[key] = unwrap(val)\n        }\n        return result\n    }\n\n    if let array = value as? [Any?] {\n        return array.compactMap { unwrap($0) }\n    }\n    return value\n}\n\n/// Recursively wraps a native Swift value into an `AnyCodable` structure,\n/// enabling flexible serialization and dynamic schema support.\n///\n/// - Parameter value: A raw value of any supported type (`Int`, `Bool`, `String`, array, dictionary, etc.).\n/// - Returns: A wrapped `AnyCodable` version of the input, preserving nested structure.\npublic func wrap(_ value: Any?) -> AnyCodable {\n    if let anyCodable = value as? AnyCodable {\n        return anyCodable\n    }\n    if let dict = value as? [String: AnyCodable] {\n        var result: [String: AnyCodable] = [:]\n        for (key, val) in dict {\n            result[key] = wrap(val)\n        }\n        return AnyCodable(result)\n    }\n    if let dict = value as? [String: Any] {\n        var result: [String: AnyCodable] = [:]\n        for (key, val) in dict {\n            result[key] = wrap(val)\n        }\n        return AnyCodable(result)\n    }\n    if let array = value as? [Any] {\n        return AnyCodable(array.map { wrap($0) })\n    }\n    return AnyCodable(value)\n}\n"
  },
  {
    "path": "Sources/ToucanSDK/Utilities/AnyCodable+Json.swift",
    "content": "//\n//  AnyCodable+Json.swift\n//  Toucan\n//\n//  Created by Viasz-Kádi Ferenc on 2025. 05. 12..\n//\n\nimport Foundation\nimport ToucanSource\n\npublic extension AnyCodable {\n    /// Recursively unwraps all nested `AnyCodable` values into Swift-native types, encodable objects are converted into [String: Any] if possible.\n    ///\n    /// - Returns: A Swift-native representation of the value, unwrapped from any nested `AnyCodable` containers.\n    func unboxed(_ encoder: JSONEncoder) -> Any {\n        switch value {\n        case let dict as [String: AnyCodable]:\n            dict.unboxed(encoder)\n        case let array as [[String: AnyCodable]]:\n            array.map { $0.unboxed(encoder) }\n        case let array as [AnyCodable]:\n            array.unboxed(encoder)\n        case let nested as AnyCodable:\n            nested.unboxed(encoder)\n        case let encodable as Encodable:\n            encodable.toJSONDictionary(encoder) ?? value ?? NSNull()\n        default:\n            value ?? NSNull()\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSDK/Utilities/Array+AnyCodable.swift",
    "content": "//\n//  Array+AnyCodable.swift\n//  Toucan\n//\n//  Created by Viasz-Kádi Ferenc on 2025. 05. 11..\n//\n\nimport Foundation\nimport ToucanSource\n\npublic extension [AnyCodable] {\n    /// Returns an array of unboxed elements by applying `unboxed()` to each element in the sequence.\n    ///\n    /// - Returns: An array containing the result of calling `unboxed()` on each element.\n    func unboxed(_ encoder: JSONEncoder) -> [Any] {\n        reduce(into: []) { result, element in\n            result.append(element.unboxed(encoder))\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSDK/Utilities/ContextKeys.swift",
    "content": "//\n//  ContextKeys.swift\n//  Toucan\n//\n//  Created by Viasz-Kádi Ferenc on 2025. 09. 04..\n//\n\n/// Root-level keys used in the rendered context bundles.\npublic enum RootContextKeys: String, CaseIterable {\n    case page\n    case iterator\n    case context\n}\n\n/// Standard keys included in the page context dictionary.\npublic enum PageContextKeys: String, CaseIterable {\n    case contents\n    case permalink\n}\n\n/// Keys for the `page.contents` dictionary.\npublic enum PageContentsKeys: String, CaseIterable {\n    case html\n    case readingTime\n    case outline\n}\n\n/// Keys for iterator metadata inside the context bundle.\npublic enum IteratorKeys: String, CaseIterable {\n    case total\n    case limit\n    case current\n    case items\n    case links\n}\n\n/// Global keys merged into the rendering context.\npublic enum GlobalContextKeys: String, CaseIterable {\n    case baseUrl\n    case generator\n    case generation\n    case site\n}\n\n/// Front matter keys related to view resolution.\npublic enum ViewFrontMatterKeys: String, CaseIterable {\n    case view\n    case views\n    case any = \"views.*\"\n}\n"
  },
  {
    "path": "Sources/ToucanSDK/Utilities/CopyManager.swift",
    "content": "//\n//  CopyManager.swift\n//  Toucan\n//\n//  Created by gerp83 on 2025. 04. 17..\n//\n\nimport FileManagerKit\nimport Foundation\n\n/// Responsible for copying static assets from various source locations into the working directory.\npublic struct CopyManager {\n\n    /// File manager abstraction for performing file operations.\n    let fileManager: FileManagerKit\n\n    /// Source configuration, containing paths to asset directories.\n    let sources: [URL]\n\n    /// The target directory where all assets should be written.\n    let destination: URL\n\n    /// Initializes a new asset writer for copying static files.\n    ///\n    /// - Parameters:\n    ///   - fileManager: The file system manager.\n    ///   - sources: Provides paths based on the source urls.\n    ///   - destination: Target directory for copying all assets.\n    public init(\n        fileManager: FileManagerKit,\n        sources: [URL],\n        destination: URL\n    ) {\n        self.fileManager = fileManager\n        self.sources = sources\n        self.destination = destination\n    }\n\n    /// Copies all default, overridden, and site-level assets into the working directory.\n    ///\n    /// - Throws: Errors from the file system if copying fails.\n    public func copy() throws {\n        for source in sources {\n            try fileManager.copyRecursively(\n                from: source,\n                to: destination\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSDK/Utilities/Dictionary+AnyCodable.swift",
    "content": "//\n//  Dictionary+AnyCodable.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 01. 31..\n//\n\nimport Foundation\nimport ToucanSource\n\npublic extension [String: AnyCodable] {\n    /// Returns a dictionary with the same keys as the original, where each value has been unwrapped or transformed using the `unboxed` method.\n    ///\n    /// - Returns: A `[String: Any]` dictionary with unboxed values.\n    func unboxed(_ encoder: JSONEncoder) -> [String: Any] {\n        reduce(into: [:]) { result, element in\n            result[element.key] = element.value.unboxed(encoder)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSDK/Utilities/Encodable+Json.swift",
    "content": "//\n//  Encodable+Json.swift\n//  Toucan\n//\n//  Created by Viasz-Kádi Ferenc on 2025. 05. 11..\n//\n\nimport Foundation\n\npublic extension Encodable {\n    /// Converts the current object to a JSON dictionary using the specified encoder.\n    ///\n    /// - Parameter encoder: The `JSONEncoder` used to encode the object.\n    /// - Returns: A dictionary representation of the object if encoding and serialization succeed; otherwise, `nil`.\n    func toJSONDictionary(_ encoder: JSONEncoder) -> [String: Any]? {\n        do {\n            let data = try encoder.encode(self)\n            let json = try JSONSerialization.jsonObject(with: data, options: [])\n            return json as? [String: Any]\n        }\n        catch {\n            return nil\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSDK/Utilities/FirstSucceeding.swift",
    "content": "//\n//  FirstSucceeding.swift\n//  Toucan\n//\n//  Created by Viasz-Kádi Ferenc on 2025. 05. 11..\n//\n\n/// Attempts to execute a sequence of throwing closures, returning the first non-nil result.\n///\n/// Iterates over the provided array of closures and executes each in order.\n/// If a closure throws an error, the error is ignored and the next closure is attempted.\n/// The function returns the first non-nil value produced by a closure, or `nil` if all closures either throw or return nil.\n///\n/// - Parameter blocks: An array of throwing closures that each return an optional value.\n/// - Returns: The first non-nil result returned by a closure, or `nil` if none succeed.\nfunc firstSucceeding<T>(_ blocks: [() throws -> T?]) -> T? {\n    for block in blocks {\n        if let result = try? block() {\n            return result\n        }\n    }\n    return nil\n}\n"
  },
  {
    "path": "Sources/ToucanSDK/Validators/BuildTargetSourceValidator.swift",
    "content": "//\n//  BuildTargetSourceValidator.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 23..\n//\n\nimport Foundation\nimport ToucanCore\nimport ToucanSerialization\nimport ToucanSource\n\nenum BuildTargetSourceValidatorError: ToucanError {\n    case duplicateContentTypes([String])\n    case noDefaultContentType\n    case multipleDefaultContentTypes([String])\n    case duplicatePipelines([String])\n    case invalidRawContentOriginPath(String)\n    case invalidRawContentOriginSlug(String)\n    case duplicateRawContentSlugs([String])\n    case duplicateBlocks([String])\n    case invalidLocale(String)\n    case invalidTimeZone(String)\n\n    case unknown(Error)\n\n    var underlyingErrors: [any Error] {\n        switch self {\n        case let .unknown(error):\n            [error]\n        default:\n            []\n        }\n    }\n\n    var logMessage: String {\n        switch self {\n        case let .duplicateContentTypes(values):\n            let items = values.map { \"`\\($0)`\" }.joined(separator: \", \")\n            return \"Duplicate content types: \\(items).\"\n        case .noDefaultContentType:\n            return \"No default content type.\"\n        case let .multipleDefaultContentTypes(values):\n            let items = values.map { \"`\\($0)`\" }.joined(separator: \", \")\n            return \"Multiple default content types: \\(items).\"\n        case let .duplicatePipelines(values):\n            let items = values.map { \"`\\($0)`\" }.joined(separator: \", \")\n            return \"Duplicate pipelines: \\(items).\"\n        case let .invalidRawContentOriginPath(path):\n            return \"Invalid path: \\(path).\"\n        case let .invalidRawContentOriginSlug(slug):\n            return \"Invalid slug: \\(slug).\"\n        case let .duplicateRawContentSlugs(values):\n            let items = values.map { \"`\\($0)`\" }.joined(separator: \", \")\n            return \"Duplicate slugs: \\(items).\"\n        case let .duplicateBlocks(values):\n            let items = values.map { \"`\\($0)`\" }.joined(separator: \", \")\n            return \"Duplicate blocks: \\(items).\"\n        case let .invalidLocale(locale):\n            return \"Invalid site locale: `\\(locale)`.\"\n        case let .invalidTimeZone(timeZone):\n            return \"Invalid site time zone: `\\(timeZone)`.\"\n        case let .unknown(error):\n            return error.localizedDescription\n        }\n    }\n\n    var userFriendlyMessage: String {\n        switch self {\n        case let .duplicateContentTypes(values):\n            let items = values.map { \"`\\($0)`\" }.joined(separator: \", \")\n            return \"Duplicate content types: \\(items).\"\n        case .noDefaultContentType:\n            return \"No default content type.\"\n        case let .multipleDefaultContentTypes(values):\n            let items = values.map { \"`\\($0)`\" }.joined(separator: \", \")\n            return \"Multiple default content types: \\(items).\"\n        case let .duplicatePipelines(values):\n            let items = values.map { \"`\\($0)`\" }.joined(separator: \", \")\n            return \"Duplicate pipelines: \\(items).\"\n        case let .invalidRawContentOriginPath(path):\n            return \"Invalid path: \\(path).\"\n        case let .invalidRawContentOriginSlug(slug):\n            return \"Invalid slug: \\(slug).\"\n        case let .duplicateRawContentSlugs(values):\n            let items = values.map { \"`\\($0)`\" }.joined(separator: \", \")\n            return \"Duplicate slugs: \\(items).\"\n        case let .duplicateBlocks(values):\n            let items = values.map { \"`\\($0)`\" }.joined(separator: \", \")\n            return \"Duplicate blocks: \\(items).\"\n        case let .invalidLocale(locale):\n            return \"Invalid site locale: `\\(locale)`.\"\n        case let .invalidTimeZone(timeZone):\n            return \"Invalid site time zone: `\\(timeZone)`.\"\n        case .unknown:\n            return \"Unknown source validator error.\"\n        }\n    }\n}\n\nstruct BuildTargetSourceValidator {\n\n    var buildTargetSource: BuildTargetSource\n\n    func validate() throws(BuildTargetSourceValidatorError) {\n        try validatePipelines()\n        try validateBlocks()\n        try validateContentTypes()\n        try validateRawContentOrigins()\n        try validateRawContents()\n    }\n\n    func validatePipelines() throws(BuildTargetSourceValidatorError) {\n        let ids = buildTargetSource.pipelines.map(\\.id)\n        let duplicates = Dictionary(grouping: ids, by: { $0 })\n            .mapValues { $0.count }\n            .filter { $1 > 1 }\n\n        if !duplicates.isEmpty {\n            throw .duplicateContentTypes(\n                duplicates.keys.map { String($0) }.sorted()\n            )\n        }\n    }\n\n    func validateBlocks() throws(BuildTargetSourceValidatorError) {\n        let names = buildTargetSource.blocks.map(\\.name)\n        let duplicates = Dictionary(grouping: names, by: { $0 })\n            .mapValues { $0.count }\n            .filter { $1 > 1 }\n\n        if !duplicates.isEmpty {\n            throw .duplicateContentTypes(\n                duplicates.keys.map { String($0) }.sorted()\n            )\n        }\n    }\n\n    func validateContentTypes() throws(BuildTargetSourceValidatorError) {\n        let ids = buildTargetSource.types.map(\\.id)\n        let duplicates = Dictionary(grouping: ids, by: { $0 })\n            .mapValues { $0.count }\n            .filter { $1 > 1 }\n\n        if !duplicates.isEmpty {\n            throw .duplicateContentTypes(\n                duplicates.keys.map { String($0) }.sorted()\n            )\n        }\n        let items = buildTargetSource.types.filter(\\.default)\n        if items.isEmpty {\n            throw .noDefaultContentType\n        }\n        if items.count > 1 {\n            throw .multipleDefaultContentTypes(items.map(\\.id).sorted())\n        }\n    }\n\n    func validateRawContentOrigins() throws(BuildTargetSourceValidatorError) {\n        let origins = buildTargetSource.rawContents.map(\\.origin)\n        for origin in origins {\n            guard origin.path.value.containsOnlyValidPathCharacters() else {\n                throw .invalidRawContentOriginPath(origin.path.value)\n            }\n            guard origin.slug.containsOnlyValidURLCharacters() else {\n                throw .invalidRawContentOriginSlug(origin.slug)\n            }\n        }\n    }\n\n    func validateRawContents() throws(BuildTargetSourceValidatorError) {\n        let slugs = buildTargetSource.rawContents.map(\\.origin.slug)\n        let duplicates = Dictionary(grouping: slugs, by: { $0 })\n            .mapValues { $0.count }\n            .filter { $1 > 1 }\n\n        if !duplicates.isEmpty {\n            throw .duplicateRawContentSlugs(\n                duplicates.keys.map { String($0) }.sorted()\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSDK/Validators/TemplateValidator.swift",
    "content": "//\n//  TemplateValidator.swift\n//  Toucan\n//\n//  Created by Viasz-Kádi Ferenc on 2025. 06. 17..\n//\n\nimport Foundation\nimport ToucanCore\nimport ToucanSource\nimport Version\n\nenum TemplateValidatorError: ToucanError {\n    case unsupportedGeneratorVersion(\n        generatorVersion: Template.Metadata.GeneratorVersion,\n        currentVersion: Version\n    )\n\n    var logMessage: String {\n        switch self {\n        case let .unsupportedGeneratorVersion(generatorVersion, currentVersion):\n            return\n                \"Unsupported generator version: `\\(generatorVersion.type)(\\(generatorVersion.value))`. Current Toucan version: \\(currentVersion).\"\n        }\n    }\n\n    var userFriendlyMessage: String {\n        logMessage\n    }\n}\n\nstruct TemplateValidator {\n\n    let version: Version\n\n    init(generatorInfo: GeneratorInfo = .current) throws {\n        self.version = generatorInfo.release\n    }\n\n    func validate(_ template: Template) throws(TemplateValidatorError) {\n        let generatorVersion = template.metadata.generatorVersion\n        let isSupported: Bool\n\n        switch generatorVersion.type {\n        case .upNextMajor:\n            let lowerBound = generatorVersion.value\n            let upperBound = Version(generatorVersion.value.major + 1, 0, 0)\n            isSupported = (lowerBound..<upperBound).contains(version)\n        case .upNextMinor:\n            let lowerBound = generatorVersion.value\n            let upperBound = Version(\n                generatorVersion.value.major,\n                generatorVersion.value.minor + 1,\n                0\n            )\n            isSupported = (lowerBound..<upperBound).contains(version)\n        case .exact:\n            isSupported = generatorVersion.value == version\n        }\n\n        if !isSupported {\n            throw .unsupportedGeneratorVersion(\n                generatorVersion: generatorVersion,\n                currentVersion: version\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSerialization/ToucanDecoder.swift",
    "content": "//\n//  ToucanDecoder.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 01. 29..\n//\n\nimport struct Foundation.Data\n\n/// A protocol representing a custom decoder capable of transforming `Data` into strongly typed models.\npublic protocol ToucanDecoder {\n    /// Decodes a `Decodable` type from raw data.\n    ///\n    /// - Parameters:\n    ///   - type: The expected type to decode (conforms to `Decodable`).\n    ///   - string: The raw `String` input (e.g., file contents as String).\n    /// - Returns: A decoded instance of the specified type.\n    /// - Throws: `ToucanDecoderError` if decoding fails or data is invalid.\n    func decode<T: Decodable>(\n        _ type: T.Type,\n        from string: String\n    ) throws(ToucanDecoderError) -> T\n\n    /// Decodes a `Decodable` type from raw data.\n    ///\n    /// - Parameters:\n    ///   - type: The expected type to decode (conforms to `Decodable`).\n    ///   - from: The raw `Data` input (e.g., file contents as Data).\n    /// - Returns: A decoded instance of the specified type.\n    /// - Throws: `ToucanDecoderError` if decoding fails or data is invalid.\n    func decode<T: Decodable>(\n        _ type: T.Type,\n        from: Data\n    ) throws(ToucanDecoderError) -> T\n}\n\npublic extension ToucanDecoder {\n    /// Decodes a `Decodable` type from raw data.\n    ///\n    /// - Parameters:\n    ///   - type: The expected type to decode (conforms to `Decodable`).\n    ///   - string: The raw `String` input (e.g., file contents as String).\n    /// - Returns: A decoded instance of the specified type.\n    /// - Throws: `ToucanDecoderError` if decoding fails or data is invalid.\n    func decode<T: Decodable>(\n        _ type: T.Type,\n        from string: String\n    ) throws(ToucanDecoderError) -> T {\n        guard let data = string.data(using: .utf8) else {\n            throw .init(\n                type: T.self,\n                error: DecodingError.dataCorrupted(\n                    .init(\n                        codingPath: [],\n                        debugDescription:\n                            \"The string cannot be represented as UTF-8 encoded data.\"\n                    )\n                )\n            )\n        }\n        return try decode(type, from: data)\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSerialization/ToucanDecoderError.swift",
    "content": "//\n//  ToucanDecoderError.swift\n//  Toucan\n//\n//  Created by gerp83 on 2025. 04. 17..\n//\n\nimport ToucanCore\n\n/// Extension providing a custom `logMessage` for decoding context.\n///\n/// Transforms the coding path into a readable string or returns the debug description if the path is empty.\nextension DecodingError.Context {\n    /// A string representation of the decoding path or debug description.\n    var logMessage: String {\n        var components = [debugDescription]\n\n        if !codingPath.isEmpty {\n            components.append(\"Coding path:\")\n\n            let path =\n                codingPath\n                .map(\\.stringValue)\n                .joined(separator: \".\")\n\n            components.append(\"`\\(path)`.\")\n        }\n\n        return components.joined(separator: \" \")\n    }\n}\n\n/// Extension to make `DecodingError` conform to the `ToucanError` protocol.\n///\n/// Provides developer and user-facing messages based on the type of decoding error.\nextension DecodingError: ToucanError {\n    /// A detailed message describing the decoding failure, including context.\n    public var logMessage: String {\n        switch self {\n        case let .dataCorrupted(context):\n            \"Data corrupted: \\(context.logMessage)\"\n        case let .keyNotFound(key, context):\n            \"Key not found: \\(key) - \\(context.logMessage)\"\n        case let .typeMismatch(type, context):\n            \"Type mismatch: \\(type) - \\(context.logMessage)\"\n        case let .valueNotFound(type, context):\n            \"Value not found: \\(type) - \\(context.logMessage)\"\n        default:\n            \"\\(self)\"\n        }\n    }\n\n    /// A localized description suitable for displaying to end users.\n    public var userFriendlyMessage: String {\n        localizedDescription\n    }\n}\n\n/// A custom error type representing a decoding failure for a specific type.\n///\n/// Wraps an optional underlying error and includes type information for logging.\npublic struct ToucanDecoderError: ToucanError {\n\n    /// The type that failed to decode.\n    let type: Any.Type\n    /// An optional underlying error providing more context.\n    let error: Error?\n\n    /// An array containing the underlying error, if any.\n    public var underlyingErrors: [any Error] {\n        error.map { [$0] } ?? []\n    }\n\n    /// A developer-facing message indicating which type failed to decode.\n    public var logMessage: String {\n        \"Type decoding error: `\\(type)`.\"\n    }\n\n    /// A user-friendly message indicating a decoding failure.\n    public var userFriendlyMessage: String {\n        \"Could not decode object.\"\n    }\n\n    /// Creates a new `ToucanDecoderError`.\n    ///\n    /// - Parameters:\n    ///   - type: The type that failed decoding.\n    ///   - error: An optional underlying error.\n    init(\n        type: Any.Type,\n        error: Error? = nil\n    ) {\n        self.type = type\n        self.error = error\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSerialization/ToucanEncoder.swift",
    "content": "//\n//  ToucanEncoder.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 03. 06..\n//\n\nimport struct Foundation.Data\n\n/// A protocol representing a custom encoder that serializes `Encodable` types into `String` output.\npublic protocol ToucanEncoder {\n    /// Encodes an object conforming to `Encodable` into a `String` representation.\n    ///\n    /// - Parameter object: The value to encode.\n    /// - Returns: A serialized string output.\n    /// - Throws: `ToucanEncoderError` if encoding fails.\n    func encode(\n        _ object: some Encodable\n    ) throws(ToucanEncoderError) -> String\n\n    /// Encodes an object conforming to `Encodable` into a `Data` representation.\n    ///\n    /// - Parameter object: The value to encode.\n    /// - Returns: The Data representation of the Encodable.\n    /// - Throws: `ToucanEncoderError` if encoding fails.\n    func encode(\n        _ object: some Encodable\n    ) throws(ToucanEncoderError) -> Data\n}\n\npublic extension ToucanEncoder {\n    /// Encodes an object conforming to `Encodable` into a `Data` representation.\n    ///\n    /// - Parameter object: The value to encode.\n    /// - Returns: The Data representation of the Encodable.\n    /// - Throws: `ToucanEncoderError` if encoding fails.\n    func encode<T: Encodable>(\n        _ object: T\n    ) throws(ToucanEncoderError) -> Data {\n        let string: String = try encode(object)\n\n        guard let data = string.data(using: .utf8) else {\n            throw ToucanEncoderError(\n                type: T.self,\n                error: EncodingError.invalidValue(\n                    string,\n                    .init(\n                        codingPath: [],\n                        debugDescription:\n                            \"The string cannot be represetned as UTF-8 encoded data.\"\n                    )\n                )\n            )\n        }\n        return data\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSerialization/ToucanEncoderError.swift",
    "content": "//\n//  ToucanEncoderError.swift\n//  Toucan\n//\n//  Created by gerp83 on 2025. 04. 17..\n//\n\nimport ToucanCore\n\n/// Extension to make `EncodingError` conform to the `ToucanError` protocol.\n///\n/// Provides developer and user-facing messages based on the encoding error.\nextension EncodingError: ToucanError {\n    /// A detailed log message representing the encoding error.\n    public var logMessage: String {\n        \"\\(self)\"\n    }\n\n    /// A localized description suitable for display to end users.\n    public var userFriendlyMessage: String {\n        localizedDescription\n    }\n}\n\n/// A custom error type representing a failure to encode a specific type.\n///\n/// Wraps an optional underlying error and includes the associated type information.\npublic struct ToucanEncoderError: ToucanError {\n\n    /// The type that failed to encode.\n    let type: Any.Type\n    /// An optional underlying error providing additional context.\n    let error: Error?\n\n    /// An array containing the underlying error, if present.\n    public var underlyingErrors: [any Error] {\n        error.map { [$0] } ?? []\n    }\n\n    /// A developer-facing message describing the type that failed to encode.\n    public var logMessage: String {\n        \"Type encoding error: `\\(type)`.\"\n    }\n\n    /// A user-facing message indicating that the object could not be encoded.\n    public var userFriendlyMessage: String {\n        \"Could not encode object.\"\n    }\n\n    /// Creates a new `ToucanEncoderError`.\n    ///\n    /// - Parameters:\n    ///   - type: The type that failed encoding.\n    ///   - error: An optional underlying error.\n    init(\n        type: Any.Type,\n        error: Error? = nil\n    ) {\n        self.type = type\n        self.error = error\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSerialization/ToucanJSONDecoder.swift",
    "content": "//\n//  ToucanJSONDecoder.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 04. 17..\n\nimport struct Foundation.Data\nimport class Foundation.JSONDecoder\n\n/// An implementation of `ToucanDecoder` that uses `JSONDecoder`.\npublic struct ToucanJSONDecoder: ToucanDecoder {\n    /// Initializes a new instance of `ToucanJSONDecoder`.\n    ///\n    /// Uses a `JSONDecoder` that allows JSON5 parsing by default.\n    public init() {}\n\n    /// Decodes a JSON or JSON5-encoded `Data` object into a strongly-typed model.\n    ///\n    /// - Parameters:\n    ///   - type: The target `Decodable` type.\n    ///   - data: Raw data to decode.\n    /// - Returns: A decoded instance of the provided type.\n    /// - Throws: `ToucanDecoderError.decoding` if decoding fails.\n    public func decode<T: Decodable>(\n        _ type: T.Type,\n        from data: Data\n    ) throws(ToucanDecoderError) -> T {\n        do {\n            let decoder = JSONDecoder()\n            decoder.allowsJSON5 = true\n            return try decoder.decode(type, from: data)\n        }\n        catch {\n            throw .init(type: T.self, error: error)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSerialization/ToucanJSONEncoder.swift",
    "content": "//\n//  ToucanJSONEncoder.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 18..\n//\n\nimport struct Foundation.Data\nimport class Foundation.JSONEncoder\n\n/// An implementation of `ToucanEncoder` that uses JSON`.\npublic struct ToucanJSONEncoder: ToucanEncoder {\n\n    /// Initializes a new instance of the JSON encoder.\n    public init() {}\n\n    /// Encodes a given `Encodable` object into a JSON `String`.\n    ///\n    /// - Parameter object: The value to encode.\n    /// - Returns: A YAML-formatted string representation of the object.\n    /// - Throws: `ToucanEncoderError.encoding` if encoding fails.\n    public func encode<T: Encodable>(\n        _ object: T\n    ) throws(ToucanEncoderError) -> String {\n        do {\n            let encoder = JSONEncoder()\n            encoder.outputFormatting = [\n                .sortedKeys,\n                .prettyPrinted,\n                .withoutEscapingSlashes,\n            ]\n            let data = try encoder.encode(object)\n            guard let string = String(data: data, encoding: .utf8) else {\n                throw ToucanEncoderError(\n                    type: T.self,\n                    error: EncodingError.invalidValue(\n                        data,\n                        .init(\n                            codingPath: [],\n                            debugDescription:\n                                \"The data cannot be represetned as UTF-8 encoded string.\"\n                        )\n                    )\n                )\n            }\n            return string\n        }\n        catch {\n            throw .init(type: T.self, error: error)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSerialization/ToucanYAMLDecoder.swift",
    "content": "//\n//  ToucanYAMLDecoder.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 04. 17..\n\nimport struct Foundation.Data\nimport class Yams.YAMLDecoder\n\n/// An implementation of `ToucanDecoder` that uses `YAMLDecoder`.\npublic struct ToucanYAMLDecoder: ToucanDecoder {\n    /// Creates a new YAML decoder instance for use in the Toucan system.\n    public init() {}\n\n    /// Decodes a YAML-formatted `Data` object into a strongly typed model.\n    ///\n    /// - Parameters:\n    ///   - type: The expected `Decodable` type.\n    ///   - data: The raw YAML data to decode.\n    /// - Returns: A decoded instance of the specified type.\n    /// - Throws: `ToucanDecoderError.decoding` if the input cannot be decoded.\n    public func decode<T: Decodable>(\n        _ type: T.Type,\n        from data: Data\n    ) throws(ToucanDecoderError) -> T {\n        do {\n            let decoder = YAMLDecoder()\n            return try decoder.decode(type, from: data)\n        }\n        catch {\n            throw .init(type: T.self, error: error)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSerialization/ToucanYAMLEncoder.swift",
    "content": "//\n//  ToucanYAMLEncoder.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 03. 06..\n//\n\nimport struct Foundation.Data\nimport class Yams.YAMLEncoder\n\n/// A n implementation of `ToucanEncoder` that uses `YAMLEncoder`.\npublic struct ToucanYAMLEncoder: ToucanEncoder {\n    /// Initializes a new instance of the YAML encoder.\n    public init() {}\n\n    /// Encodes a given `Encodable` object into a YAML `String`.\n    ///\n    /// - Parameter object: The value to encode.\n    /// - Returns: A YAML-formatted string representation of the object.\n    /// - Throws: `ToucanEncoderError.encoding` if encoding fails.\n    public func encode<T: Encodable>(\n        _ object: T\n    ) throws(ToucanEncoderError) -> String {\n        do {\n            let encoder = YAMLEncoder()\n            encoder.options.sortKeys = true\n            return try encoder.encode(object)\n        }\n        catch {\n            throw .init(type: T.self, error: error)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Errors/ObjectLoaderError.swift",
    "content": "//\n//  ObjectLoaderError.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 21..\n//\n\nimport struct Foundation.URL\nimport ToucanCore\n\n/// A custom error type representing a failure to load or decode a file.\n///\n/// Wraps the file URL and an optional underlying error for context and debugging.\npublic struct ObjectLoaderError: ToucanError {\n\n    /// The URL of the file that caused the error.\n    let url: URL\n    /// The underlying error that occurred during loading or decoding.\n    let error: Error?\n\n    /// An array containing the underlying error if present.\n    public var underlyingErrors: [Error] {\n        error.map { [$0] } ?? []\n    }\n\n    /// A developer-facing log message including the path of the failed file.\n    public var logMessage: String {\n        \"File issue at: `\\(url.path())`.\"\n    }\n\n    /// A user-facing message indicating a loading failure.\n    public var userFriendlyMessage: String {\n        \"Could not load object.\"\n    }\n\n    /// Initializes a new `ObjectLoaderError`.\n    ///\n    /// - Parameters:\n    ///   - url: The URL of the file involved in the error.\n    ///   - error: An optional underlying error.\n    init(\n        url: URL,\n        error: Error? = nil\n    ) {\n        self.url = url\n        self.error = error\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Errors/SourceLoaderError.swift",
    "content": "//\n//  SourceLoaderError.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 21..\n//\n\nimport ToucanCore\n\n/// A custom error type representing failures during the source loading process.\n///\n/// Wraps the type of failure and an optional underlying error for context.\npublic struct SourceLoaderError: ToucanError {\n\n    /// A string representing the type of component that failed to load.\n    let type: String\n    /// An optional error providing additional context about the failure.\n    let error: Error?\n\n    /// An array containing the underlying error if available, used for nested error representation.\n    public var underlyingErrors: [Error] {\n        error.map { [$0] } ?? []\n    }\n\n    /// A developer-facing message indicating the type that failed to load.\n    public var logMessage: String {\n        \"Could not load: `\\(type)`.\"\n    }\n\n    /// A user-facing message indicating a generic failure to load source content.\n    public var userFriendlyMessage: String {\n        \"Could not load source.\"\n    }\n\n    /// Initializes a new `SourceLoaderError`.\n    ///\n    /// - Parameters:\n    ///   - type: A string indicating the failed component type.\n    ///   - error: An optional error that triggered the failure.\n    init(\n        type: String,\n        error: Error? = nil\n    ) {\n        self.type = type\n        self.error = error\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Errors/TemplateLoaderError.swift",
    "content": "//\n//  TemplateLoaderError.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 21..\n//\n\nimport ToucanCore\n\n/// A custom error type representing failures during the source loading process.\n///\n/// Wraps the type of failure and an optional underlying error for context.\npublic struct TemplateLoaderError: ToucanError {\n\n    /// A string representing the type of component that failed to load.\n    let type: String\n    /// An optional error providing additional context about the failure.\n    let error: Error?\n\n    /// An array containing the underlying error if available, used for nested error representation.\n    public var underlyingErrors: [Error] {\n        error.map { [$0] } ?? []\n    }\n\n    /// A developer-facing message indicating the type that failed to load.\n    public var logMessage: String {\n        \"Could not load: `\\(type)`.\"\n    }\n\n    /// A user-facing message indicating a generic failure to load source content.\n    public var userFriendlyMessage: String {\n        \"Could not load template metadata.\"\n    }\n\n    /// Initializes a new `SourceLoaderError`.\n    ///\n    /// - Parameters:\n    ///   - type: A string indicating the failed component type.\n    ///   - error: An optional error that triggered the failure.\n    init(\n        type: String,\n        error: Error? = nil\n    ) {\n        self.type = type\n        self.error = error\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Extensions/Decoder+Validate.swift",
    "content": "//\n//  Decoder+Validate.swift\n//  Toucan\n//\n//  Created by Ferenc Viasz-Kadi on 2025. 08. 19..\n//\n\nextension Decoder {\n\n    /// Validates that the top-level decoded object contains no unknown keys outside the given `CodingKey` set.\n    ///\n    /// This method inspects the raw decoded object as a `[String: AnyCodable]` dictionary and compares its keys\n    /// against the expected cases defined in the provided `CodingKey` type. If any extra keys are found that are\n    /// not part of the enum, it throws a `DecodingError.dataCorruptedError`.\n    ///\n    /// - Parameter keyType: A `CodingKey` type that conforms to `CaseIterable`. This type defines the set of known/expected keys.\n    /// - Throws: A `DecodingError.dataCorruptedError` if unexpected keys are found in the decoded object.\n    public func validateUnknownKeys<K: CodingKey & CaseIterable>(\n        keyType: K.Type\n    ) throws {\n        guard let _ = try? container(keyedBy: keyType) else {\n            return\n        }\n\n        // Decode raw dictionary\n        let raw = try singleValueContainer().decode([String: AnyCodable].self)\n\n        let expectedKeys = Set(K.allCases.map { $0.stringValue })\n        let actualKeys = Set(raw.keys)\n\n        let unknownKeys = actualKeys.subtracting(expectedKeys)\n\n        if !unknownKeys.isEmpty {\n            let inputKeys =\n                unknownKeys\n                .sorted()\n                .map { \"`\\($0)`\" }\n                .joined(separator: \", \")\n\n            let expectedKeys =\n                expectedKeys\n                .sorted()\n                .map { \"`\\($0)`\" }\n                .joined(separator: \", \")\n\n            throw DecodingError.dataCorrupted(\n                .init(\n                    codingPath: codingPath,\n                    debugDescription:\n                        \"Unknown keys found: \\(inputKeys). Expected keys: \\(expectedKeys).\"\n                )\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Extensions/Dictionary+AnyCodable.swift",
    "content": "//\n//  Dictionary+AnyCodable.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 18..\n//\n\nimport struct Foundation.Date\nimport class Foundation.DateFormatter\n\npublic extension [String: AnyCodable] {\n    func value<T>(_ keyPath: String, as _: T.Type) -> T? {\n        let keys = keyPath.split(separator: \".\").map { String($0) }\n\n        guard !keys.isEmpty else {\n            return nil\n        }\n        var currentDict: [String: AnyCodable] = self\n\n        for key in keys.dropLast() {\n            if let dict = currentDict[key]?.value as? [String: AnyCodable] {\n                currentDict = dict\n            }\n            else {\n                return nil\n            }\n        }\n        return currentDict[keys.last!]?.value as? T\n    }\n\n    func bool(_ keyPath: String) -> Bool? {\n        value(keyPath, as: Bool.self)\n    }\n\n    func int(_ keyPath: String) -> Int? {\n        value(keyPath, as: Int.self)\n    }\n\n    func double(_ keyPath: String) -> Double? {\n        value(keyPath, as: Double.self)\n    }\n\n    func string(\n        _ keyPath: String,\n        allowingEmptyValue: Bool = false\n    ) -> String? {\n        let result = value(keyPath, as: String.self)\n        if allowingEmptyValue {\n            return result\n        }\n        return (result ?? \"\").isEmpty ? nil : result\n    }\n\n    func array<T>(_ keyPath: String, as _: T.Type) -> [T] {\n        value(keyPath, as: [T].self) ?? []\n    }\n\n    func dict(_ keyPath: String) -> [String: AnyCodable] {\n        value(keyPath, as: [String: AnyCodable].self) ?? [:]\n    }\n\n    func date(_ keyPath: String, formatter: DateFormatter) -> Date? {\n        guard let rawDate = value(keyPath, as: String.self) else {\n            return nil\n        }\n        return formatter.date(from: rawDate)\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Extensions/FileManagerKit+Extensions.swift",
    "content": "//\n//  FileManagerKit+Extensions.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 04. 17..\n\nimport FileManagerKit\nimport struct Foundation.URL\n\nprivate extension URL {\n    /// Computes a relative path from the current URL (`self`) to another base URL.\n    ///\n    /// This method compares the standardized path components of both URLs,\n    /// identifies their shared prefix, and removes it from the current URL path\n    /// to return a relative path string.\n    ///\n    /// - Parameter url: The base URL to which the path should be made relative.\n    /// - Returns: A relative path string from `url` to `self`.\n    func relativePath(to url: URL) -> String {\n        // Break both paths into components (standardized removes '.', '..', etc.)\n        let components = standardized.pathComponents\n        let baseComponents = url.standardized.pathComponents\n\n        // Determine how many leading components are shared between both paths\n        let commonPrefixCount = zip(components, baseComponents)\n            .prefix { $0 == $1 }\n            .count\n\n        // Remove the common prefix to compute the relative path\n        let relativeComponents = components.dropFirst(commonPrefixCount)\n\n        // Join the remaining components with \"/\" to form the relative path\n        return relativeComponents.joined(separator: \"/\")\n    }\n}\n\npublic extension FileManagerKit {\n    /// Find files in the specified directory that match the given name and extensions criteria.\n    ///\n    /// - Parameters: url: The URL of the directory to search.\n    /// - Returns: An array of file names that match the specified criteria.\n    func find(\n        name: String? = nil,\n        extensions: [String]? = nil,\n        recursively: Bool = false,\n        skipHiddenFiles: Bool = true,\n        at url: URL\n    ) -> [String] {\n        var items: [String] = []\n        if recursively {\n            items = listDirectoryRecursively(at: url)\n                .map {\n                    // Convert to a relative path based on the root URL\n                    $0.relativePath(to: url)\n                }\n        }\n        else {\n            items = listDirectory(at: url)\n        }\n\n        if skipHiddenFiles {\n            items = items.filter { !$0.hasPrefix(\".\") }\n        }\n\n        return items.filter { fileName in\n            let fileURL = URL(fileURLWithPath: fileName)\n            let baseName = fileURL.deletingPathExtension().lastPathComponent\n            let ext = fileURL.pathExtension\n\n            switch (name, extensions) {\n            case (nil, nil):\n                return true\n            case (let name?, nil):\n                return baseName == name\n            case (nil, let extensions?):\n                return extensions.contains(ext)\n            case let (name?, extensions?):\n                return baseName == name && extensions.contains(ext)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Loaders/BuildTargetSourceLoader.swift",
    "content": "//\n//  BuildTargetSourceLoader.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 04. 04..\n//\n\nimport FileManagerKit\nimport Foundation\nimport Logging\nimport ToucanCore\nimport ToucanSerialization\n\n/// Loads and processes various parts of a build target's source bundle.\n///\n/// Uses dependency-injected tools to fetch, decode, and construct structured data from source files.\npublic struct BuildTargetSourceLoader {\n\n    /// The URL of the root source directory.\n    var sourceURL: URL\n    /// Metadata describing the current build target.\n    var target: Target\n\n    /// A utility for accessing and searching the file system.\n    var fileManager: FileManagerKit\n\n    /// Encoder and decoder for serializing and deserializing content.\n    var encoder: ToucanEncoder\n    var decoder: ToucanDecoder\n\n    /// Logger instance for emitting structured debug information.\n    var logger: Logger\n\n    /// Initializes a new instance of `BuildTargetSourceLoader`.\n    ///\n    /// - Parameters:\n    ///   - sourceURL: The root directory containing source files.\n    ///   - target: The build target metadata.\n    ///   - fileManager: File system access helper.\n    ///   - encoder: The encoder used for serialization.\n    ///   - decoder: The decoder used for deserialization.\n    ///   - logger: Optional logger for debugging and diagnostics.\n    public init(\n        sourceURL: URL,\n        target: Target,\n        fileManager: FileManagerKit,\n        encoder: ToucanEncoder,\n        decoder: ToucanDecoder,\n        logger: Logger = .subsystem(\"source-loader\")\n    ) {\n        self.sourceURL = sourceURL\n        self.target = target\n        self.fileManager = fileManager\n        self.encoder = encoder\n        self.decoder = decoder\n        self.logger = logger\n    }\n\n    /// Loads raw contents from the source using the provided configuration.\n    ///\n    /// - Parameter config: The configuration object used to determine content locations.\n    /// - Returns: An array of `RawContent` objects.\n    /// - Throws: A `SourceLoaderError` if loading fails.\n    private func loadRawContents(\n        using config: Config\n    ) throws(SourceLoaderError) -> [RawContent] {\n        do {\n            let locations = BuiltTargetSourceLocations(\n                sourceURL: sourceURL,\n                config: config\n            )\n            let rawContentsLoader = RawContentLoader(\n                contentsURL: locations.contentsURL,\n                assetsPath: config.contents.assets.path,\n                decoder: .init(),\n                markdownParser: .init(decoder: decoder),\n                fileManager: fileManager\n            )\n            return try rawContentsLoader.load()\n        }\n        catch {\n            throw .init(type: \"RawContent\", error: error)\n        }\n    }\n\n    /// Loads a single Codable object of the specified type from a named file at a given URL.\n    ///\n    /// - Parameters:\n    ///   - type: The type to decode.\n    ///   - name: The name of the file to load.\n    ///   - url: The directory URL to search within.\n    /// - Returns: An instance of the decoded type.\n    /// - Throws: A `SourceLoaderError` if loading or decoding fails.\n    private func load<T: Codable>(\n        type: T.Type,\n        named name: String,\n        at url: URL\n    ) throws(SourceLoaderError) -> T {\n        do {\n            return try ObjectLoader(\n                url: url,\n                locations: fileManager.find(\n                    name: name,\n                    extensions: [\"yaml\", \"yml\"],\n                    at: url\n                ),\n                encoder: encoder,\n                decoder: decoder\n            )\n            .load(type)\n        }\n        catch {\n            throw .init(type: \"ObjectLoader\", error: error)\n        }\n    }\n\n    /// Loads an array of Decodable objects of the specified type from YAML files at a given URL.\n    ///\n    /// - Parameters:\n    ///   - type: The type to decode.\n    ///   - url: The directory URL to search within.\n    /// - Returns: An array of decoded objects.\n    /// - Throws: A `SourceLoaderError` if loading or decoding fails.\n    private func load<T: Decodable>(\n        type: T.Type,\n        at url: URL\n    ) throws(SourceLoaderError) -> [T] {\n        do {\n            return try ObjectLoader(\n                url: url,\n                locations: fileManager.find(\n                    extensions: [\"yaml\", \"yml\"],\n                    at: url\n                ),\n                encoder: encoder,\n                decoder: decoder\n            )\n            .load(type)\n        }\n        catch {\n            throw .init(type: \"\\(type)\", error: error)\n        }\n    }\n\n    /// Loads the main configuration object from the source.\n    ///\n    /// The loader first attempts to locate a configuration file named `config-{target.name}` at the computed source location.\n    /// - If the file exists, it attempts to parse and load it. If parsing fails, a `SourceLoaderError` is thrown.\n    /// - If the file does not exist, the loader falls back to load the default `config` file from the same location.\n    /// - If neither configuration file can be successfully loaded, a `SourceLoaderError` is thrown with detailed context.\n    ///\n    /// - Returns: A `Config` object loaded from the source.\n    /// - Throws: A `SourceLoaderError` if loading fails or parsing the located configuration file fails.\n    func loadConfig() throws(SourceLoaderError) -> Config {\n        do {\n            let configURL = sourceURL.appendingPathIfPresent(target.config)\n            let targetConfigName = \"config-\\(target.name)\"\n            let targetConfigLocation =\n                fileManager\n                .find(extensions: [\"yaml\", \"yml\"], at: configURL)\n                .first { $0.hasPrefix(targetConfigName) }\n\n            if targetConfigLocation != nil {\n                return try load(\n                    type: Config.self,\n                    named: targetConfigName,\n                    at: configURL\n                )\n            }\n\n            return try load(\n                type: Config.self,\n                named: \"config\",\n                at: configURL\n            )\n        }\n        catch {\n            throw .init(type: \"Config\", error: error)\n        }\n    }\n\n    /// Constructs the locations object based on the configuration.\n    ///\n    /// - Parameter config: The loaded configuration.\n    /// - Returns: A `BuiltTargetSourceLocations` instance.\n    func getLocations(\n        using config: Config\n    ) -> BuiltTargetSourceLocations {\n        .init(\n            sourceURL: sourceURL,\n            config: config\n        )\n    }\n\n    /// Loads the site settings from the specified locations.\n    ///\n    /// - Parameter locations: The source locations to use.\n    /// - Returns: A `Settings` object.\n    /// - Throws: A `SourceLoaderError` if loading fails.\n    func loadSettings(\n        using locations: BuiltTargetSourceLocations\n    ) throws(SourceLoaderError) -> Settings {\n        do {\n            return try load(\n                type: Settings.self,\n                named: \"site\",\n                at: locations.siteSettingsURL\n            )\n        }\n        catch {\n            throw .init(type: \"Settings\", error: error)\n        }\n    }\n\n    /// Loads pipeline definitions from the specified locations.\n    ///\n    /// - Parameter locations: The source locations to use.\n    /// - Returns: An array of `Pipeline` objects.\n    /// - Throws: A `SourceLoaderError` if loading fails.\n    func loadPipelines(\n        using locations: BuiltTargetSourceLocations\n    ) throws(SourceLoaderError) -> [Pipeline] {\n        try load(\n            type: Pipeline.self,\n            at: locations.pipelinesURL\n        )\n        .sorted { $0.id < $1.id }\n    }\n\n    /// Loads content types\n    ///\n    /// - Parameter locations: The source locations to use.\n    /// - Returns: An array of `ContentType` objects.\n    /// - Throws: A `SourceLoaderError` if loading fails.\n    func loadTypes(\n        using locations: BuiltTargetSourceLocations\n    ) throws(SourceLoaderError) -> [ContentType] {\n        try load(\n            type: ContentType.self,\n            at: locations.typesURL\n        )\n        .sorted { $0.id < $1.id }\n    }\n\n    /// Loads block directives from the specified locations.\n    ///\n    /// - Parameter locations: The source locations to use.\n    /// - Returns: An array of `Block` objects.\n    /// - Throws: A `SourceLoaderError` if loading fails.\n    func loadBlocks(\n        using locations: BuiltTargetSourceLocations\n    ) throws(SourceLoaderError) -> [Block] {\n        try load(\n            type: Block.self,\n            at: locations.blocksURL\n        )\n        .sorted { $0.name < $1.name }\n    }\n\n    /// Loads and processes source content from the specified source URL.\n    /// This function retrieves configuration, settings, content definitions, block directives,\n    /// and raw contents, then transforms them into structured content.\n    ///\n    /// - Returns: A `BuildTargetSource` containing the loaded and processed data.\n    /// - Throws: An error if any of the loading operations fail.\n    public func load() throws(SourceLoaderError) -> BuildTargetSource {\n        let config = try loadConfig()\n        let locations = getLocations(using: config)\n        let settings = try loadSettings(using: locations)\n        let pipelines = try loadPipelines(using: locations)\n        let types = try loadTypes(using: locations)\n        let blocks = try loadBlocks(using: locations)\n        let rawContents = try loadRawContents(using: config)\n\n        return .init(\n            locations: locations,\n            target: target,\n            config: config,\n            settings: settings,\n            pipelines: pipelines,\n            types: types,\n            rawContents: rawContents,\n            blockDirectives: blocks\n        )\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Loaders/ObjectLoader.swift",
    "content": "//\n//  ObjectLoader.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 16..\n//\n\nimport Foundation\nimport Logging\nimport ToucanCore\nimport ToucanSerialization\n\n/// A utility to load and decode objects from files using a specified set of encoders and decoders.\npublic struct ObjectLoader {\n\n    /// The base directory where the files are located.\n    let url: URL\n\n    /// A list of relative paths (from `url`) to the files to be loaded.\n    let locations: [String]\n\n    /// An encoder used for interpreting Swift models into data.\n    let encoder: ToucanEncoder\n\n    /// A decoder used for interpreting data into Swift models.\n    let decoder: ToucanDecoder\n\n    /// Logger instance for emitting debug output during loading.\n    let logger: Logger\n\n    /// Initializes a new `ObjectLoader` instance.\n    ///\n    /// - Parameters:\n    ///   - url: The base directory of the files.\n    ///   - locations: A list of relative paths to the files.\n    ///   - encoder: Encoder for serializing intermediate data.\n    ///   - decoder: Decoder for parsing file contents into models.\n    ///   - logger: Optional logger for debugging purposes.\n    public init(\n        url: URL,\n        locations: [String],\n        encoder: ToucanEncoder,\n        decoder: ToucanDecoder,\n        logger: Logger = .subsystem(\"object-loader\")\n    ) {\n        self.url = url\n        self.locations = locations\n        self.encoder = encoder\n        self.decoder = decoder\n        self.logger = logger\n    }\n\n    /// Loads and decodes each file separately into an array of the specified type.\n    ///\n    /// - Parameter value: The `Codable` type to decode each file into.\n    /// - Returns: An array of decoded objects.\n    /// - Throws: An `ObjectLoaderError` if reading or decoding any file fails.\n    public func load<T: Decodable>(\n        _ value: T.Type\n    ) throws(ObjectLoaderError) -> [T] {\n        logger.debug(\n            \"Loading each \\(type(of: value)) files (\\(locations)) at: `\\(url.absoluteString)`\"\n        )\n\n        var lastURL: URL?\n        var result: [T] = []\n\n        do {\n            for location in locations {\n                let fileURL = url.appendingPathIfPresent(location)\n                lastURL = fileURL\n                let data = try Data(contentsOf: fileURL)\n                let decoded = try decoder.decode(T.self, from: data)\n\n                result.append(decoded)\n            }\n        }\n        catch {\n            throw .init(\n                url: lastURL ?? url,\n                error: error\n            )\n        }\n\n        return result\n    }\n\n    /// Loads, merges, and decodes multiple files into a single instance of the specified type.\n    ///\n    /// - Parameter value: The `Codable` type to decode the combined YAML data into.\n    /// - Returns: A decoded object of the specified type.\n    /// - Throws: An `ObjectLoaderError` if reading, merging, or decoding fails.\n    public func load<T: Codable>(\n        _ value: T.Type\n    ) throws(ObjectLoaderError) -> T {\n        logger.debug(\n            \"Loading and combining \\(type(of: value)) files (\\(locations)) at: `\\(url.absoluteString)`\"\n        )\n\n        var lastURL: URL?\n        var combinedRawCodableObject: [String: AnyCodable] = [:]\n\n        do {\n            for location in locations {\n                let fileURL = url.appendingPathIfPresent(location)\n                lastURL = fileURL\n                let data = try Data(contentsOf: fileURL)\n                let decoded = try decoder.decode(\n                    [String: AnyCodable].self,\n                    from: data\n                )\n\n                combinedRawCodableObject =\n                    combinedRawCodableObject.recursivelyMerged(with: decoded)\n            }\n\n            // TODO: Tries to decode 0 files too\n            let data: Data = try encoder.encode(combinedRawCodableObject)\n            return try decoder.decode(T.self, from: data)\n        }\n        catch {\n            throw .init(\n                url: lastURL ?? url,\n                error: error\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Loaders/RawContentLoader.swift",
    "content": "//\n//  RawContentLoader.swift\n//  Toucan\n//\n//  Created by Viasz-Kádi Ferenc on 2025. 03. 03..\n//\n\nimport FileManagerKit\nimport Foundation\nimport Logging\nimport ToucanCore\nimport ToucanSerialization\n\n/// A utility structure responsible for loading and parsing raw content files\npublic struct RawContentLoader {\n\n    /// Source configuration.\n    let contentsURL: URL\n\n    /// The relative path where asset files are expected to be found.\n    let assetsPath: String\n\n    /// Decoder used to decode YAML files.\n    let decoder: ToucanYAMLDecoder\n\n    /// A parser responsible for processing front matter data.\n    let markdownParser: MarkdownParser\n\n    /// A file manager instance for handling file operations.\n    let fileManager: FileManagerKit\n\n    /// The logger instance\n    let logger: Logger\n\n    /// Creates a new instance of `RawContentLoader` with the provided dependencies.\n    ///\n    /// - Parameters:\n    ///   - contentsURL: The base URL where content files are located.\n    ///   - assetsPath: The relative path to the directory containing asset files.\n    ///   - decoder: A decoder used to parse YAML content from files.\n    ///   - markdownParser: A parser used to extract front matter and body from Markdown files.\n    ///   - fileManager: An instance responsible for file system operations.\n    ///   - logger: A logger instance used for recording diagnostic messages. Defaults to a subsystem-specific logger.\n    public init(\n        contentsURL: URL,\n        assetsPath: String,\n        decoder: ToucanYAMLDecoder,\n        markdownParser: MarkdownParser,\n        fileManager: FileManagerKit,\n        logger: Logger = .subsystem(\"raw-content-loader\")\n    ) {\n        self.contentsURL = contentsURL\n        self.assetsPath = assetsPath\n        self.decoder = decoder\n        self.markdownParser = markdownParser\n        self.fileManager = fileManager\n        self.logger = logger\n    }\n\n    /// Recursively finds all assets in the given directory.\n    ///\n    /// - Parameter url: The URL of the directory to search.\n    /// - Returns: A sorted list of relative asset file paths.\n    private func locateAssets(\n        at url: URL\n    ) -> [String] {\n        fileManager.find(recursively: true, at: url).sorted()\n    }\n\n    /// Recursively traverses the content directory to locate index-based content definitions.\n    ///\n    /// - Parameters:\n    ///   - contentsURL: The base directory for contents.\n    ///   - slug: The accumulated slug segments (used to form the output slug).\n    ///   - path: The accumulated path segments (used to navigate the file system).\n    /// - Returns: A list of discovered `RawContentLocation` objects.\n    private func locateRawContentsOrigins(\n        at contentsURL: URL,\n        slug: [String] = [],\n        path: [String] = []\n    ) -> [Origin] {\n        var result: [Origin] = []\n        let currentPath = Path(path.joined(separator: \"/\"))\n        let currentSlug = Path(slug.joined(separator: \"/\"))\n            .trimmingBracketsContent()\n        let currentURL = contentsURL.appendingPathIfPresent(currentPath.value)\n\n        logger.trace(\n            \"Trying to locate raw content item.\",\n            metadata: [\n                \"contentsURL\": .string(contentsURL.absoluteString),\n                \"path\": .string(currentPath.value),\n                \"slug\": .string(currentSlug),\n            ]\n        )\n\n        if hasIndex(at: currentURL) {\n            let origin = Origin(\n                path: currentPath,\n                slug: currentSlug\n            )\n            logger.debug(\n                \"Raw content item found with index.\",\n                metadata: [\n                    \"contentsURL\": .string(contentsURL.absoluteString),\n                    \"path\": .string(currentPath.value),\n                    \"slug\": .string(currentSlug),\n                ]\n            )\n            result.append(origin)\n        }\n\n        let list = fileManager.listDirectory(at: currentURL)\n        for item in list {\n            var newSlug = slug\n            let newPath = path + [item]\n            let childURL = currentURL.appendingPathIfPresent(item)\n\n            if !hasNoIndex(item: item, at: childURL) {\n                logger.trace(\n                    \"Raw content item has no index file or bracket.\",\n                    metadata: [\n                        \"contentsURL\": .string(contentsURL.absoluteString),\n                        \"path\": .string(currentPath.value),\n                        \"slug\": .string(currentSlug),\n                    ]\n                )\n                newSlug += [item]\n            }\n\n            result += locateRawContentsOrigins(\n                at: contentsURL,\n                slug: newSlug,\n                path: newPath\n            )\n        }\n        return result\n    }\n\n    // MARK: - index helpers\n\n    /// Finds index files within a directory.\n    ///\n    /// - Parameter url: The directory URL to search in.\n    /// - Returns: A list of index filenames matching supported extensions.\n    private func getIndexes(\n        at url: URL\n    ) -> [String] {\n        fileManager.find(\n            name: \"index\",\n            extensions: [\"yaml\", \"yml\", \"markdown\", \"md\"],\n            at: url\n        )\n    }\n\n    /// Checks whether a given directory contains index files.\n    ///\n    /// - Parameter url: The directory URL to check.\n    /// - Returns: `true` if index files exist, `false` otherwise.\n    private func hasIndex(\n        at url: URL\n    ) -> Bool {\n        !getIndexes(at: url).isEmpty\n    }\n\n    /// Determines if a directory should be excluded based on a noindex marker or name brackets.\n    ///\n    /// - Parameters:\n    ///   - item: The name of the directory.\n    ///   - url: The URL of the directory.\n    /// - Returns: `true` if the directory should be skipped, `false` otherwise.\n    private func hasNoIndex(\n        item: String,\n        at url: URL\n    ) -> Bool {\n        // Skip folders that have a noindex file or bracket marker\n        let noindexFilePaths = fileManager.find(\n            name: \"noindex\",\n            extensions: [\"yaml\", \"yml\"],\n            at: url\n        )\n        let decodedItem = item.removingPercentEncoding ?? \"\"\n        let skip = decodedItem.hasPrefix(\"[\") && decodedItem.hasSuffix(\"]\")\n\n        return skip || !noindexFilePaths.isEmpty\n    }\n\n    // MARK: - file load\n\n    /// Loads the contents of a file as a UTF-8 string.\n    ///\n    /// - Parameter url: The URL of the file to read.\n    /// - Returns: The file content as a string.\n    /// - Throws: An error if the file cannot be read.\n    private func loadContentsOfFile(\n        at url: URL\n    ) throws -> String {\n        try String(contentsOf: url, encoding: .utf8)\n    }\n\n    /// Loads and parses a Markdown file, extracting front matter and content.\n    ///\n    /// - Parameter url: The URL of the Markdown file.\n    /// - Returns: A `Markdown` object with parsed content.\n    /// - Throws: An error if the file cannot be read or parsed.\n    private func loadMarkdownFile(\n        at url: URL\n    ) throws -> Markdown {\n        let rawMarkdown = try loadContentsOfFile(at: url)\n        return try markdownParser.parse(rawMarkdown)\n    }\n\n    /// Loads and decodes a YAML file into a dictionary.\n    ///\n    /// - Parameter url: The URL of the YAML file.\n    /// - Returns: A dictionary of key-value pairs from the YAML content.\n    /// - Throws: An error if the file cannot be read or decoded.\n    private func loadYAMLFile(\n        at url: URL\n    ) throws -> [String: AnyCodable] {\n        let rawContents = try loadContentsOfFile(at: url)\n        return try decoder.decode([String: AnyCodable].self, from: rawContents)\n    }\n\n    // MARK: - locate\n\n    /// Locates all raw content entries under a specified base URL.\n    ///\n    /// Each entry is derived from a folder containing one or more valid index files (Markdown/YAML).\n    /// Subdirectories marked with `noindex.yaml|yml` are skipped.\n    ///\n    /// - Returns: A list of `Origin` objects, sorted by slug.\n    func locateOrigins() -> [Origin] {\n        locateRawContentsOrigins(\n            at: contentsURL\n        )\n        .sorted { $0.slug < $1.slug }\n    }\n\n    /// Loads a single raw content item from the specified origin.\n    ///\n    /// - Parameter origin: The origin metadata from which to load the content.\n    /// - Returns: A populated `RawContent` instance.\n    /// - Throws: An error if the content cannot be loaded or parsed.\n    func loadRawContent(\n        at origin: Origin\n    ) throws -> RawContent {\n        var frontMatter: [String: AnyCodable] = [:]\n        var contents = \"\"\n        var lastModificationDate: Date?\n\n        let currentURL = contentsURL.appendingPathIfPresent(\n            origin.path.value\n        )\n\n        let indexFiles = getIndexes(at: currentURL).sorted()\n\n        for indexFile in indexFiles {\n            let indexURL = currentURL.appendingPathIfPresent(indexFile)\n\n            if let existingDate = lastModificationDate {\n                lastModificationDate = try max(\n                    existingDate,\n                    fileManager.modificationDate(at: indexURL)\n                )\n            }\n            else {\n                lastModificationDate = try fileManager.modificationDate(\n                    at: indexURL\n                )\n            }\n\n            switch true {\n            case indexFile.hasSuffix(\"markdown\"),\n                indexFile.hasSuffix(\"md\"):\n                logger.trace(\n                    \"Loading index Markdown file\",\n                    metadata: [\n                        \"path\": .string(origin.path.value),\n                        \"slug\": .string(origin.slug),\n                        \"file\": .string(indexFile),\n                    ]\n                )\n\n                let markdown = try loadMarkdownFile(at: indexURL)\n                frontMatter = frontMatter.recursivelyMerged(\n                    with: markdown.frontMatter\n                )\n                contents = markdown.contents\n            case indexFile.hasSuffix(\"yaml\"),\n                indexFile.hasSuffix(\"yml\"):\n                logger.trace(\n                    \"Loading index YAML file\",\n                    metadata: [\n                        \"path\": .string(origin.path.value),\n                        \"slug\": .string(origin.slug),\n                        \"file\": .string(indexFile),\n                    ]\n                )\n\n                let yaml = try loadYAMLFile(at: indexURL)\n                frontMatter = frontMatter.recursivelyMerged(with: yaml)\n            default:\n                logger.warning(\n                    \"The content has no index file.\",\n                    metadata: [\n                        \"path\": .string(origin.path.value),\n                        \"slug\": .string(origin.slug),\n                    ]\n                )\n                continue\n            }\n        }\n\n        let modificationDate = lastModificationDate ?? Date()\n        let assetsURL = currentURL.appendingPathIfPresent(assetsPath)\n        let assets = locateAssets(at: assetsURL)\n\n        return RawContent(\n            origin: origin,\n            markdown: .init(\n                frontMatter: frontMatter,\n                contents: contents\n            ),\n            lastModificationDate: modificationDate.timeIntervalSince1970,\n            assetsPath: assetsPath,\n            assets: assets.sorted()\n        )\n    }\n\n    /// Loads raw content items from a set of predefined locations.\n    ///\n    /// This function iterates over a collection of locations, resolves each into a `RawContent` item,\n    /// and collects them into an array.\n    ///\n    /// - Returns: An array of `RawContent` objects representing the loaded items.\n    /// - Throws: An error if any of the content items cannot be resolved.\n    public func load() throws -> [RawContent] {\n        logger.debug(\n            \"Loading raw contents.\",\n            metadata: [\n                \"path\": .string(contentsURL.path())\n            ]\n        )\n        let origins = locateOrigins()\n\n        return try origins.map {\n            return try loadRawContent(at: $0)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Loaders/TemplateLoader.swift",
    "content": "//\n//  TemplateLoader.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 21..\n//\n\nimport FileManagerKit\nimport struct Foundation.URL\nimport ToucanSerialization\nimport Logging\nimport ToucanCore\n\n/// A loader responsible for building a `Template` by collecting assets and templates from various locations.\npublic struct TemplateLoader {\n\n    /// The file system locations relevant to the template loading process.\n    let locations: BuiltTargetSourceLocations\n    /// The list of file extensions considered as templates.\n    let extensions: [String]\n    /// The file manager utility used to search and retrieve files.\n    let fileManager: FileManagerKit\n\n    let encoder: ToucanEncoder\n    let decoder: ToucanDecoder\n    let logger: Logger\n\n    /// Initializes a new instance for handling template rendering with the given configuration.\n    ///\n    /// - Parameters:\n    ///   - locations: A set of built target source locations where templates are located.\n    ///   - extensions: An optional list of file extensions to be considered as templates. Defaults to [\"mustache\", \"html\"].\n    ///   - fileManager: File manager instance to access the file system.\n    ///   - encoder: Encoder used to serialize data for templates.\n    ///   - decoder: Decoder used to deserialize data for templates.\n    ///   - logger: Logger instance for logging template operations.\n    public init(\n        locations: BuiltTargetSourceLocations,\n        extensions: [String] = [\"mustache\", \"html\"],\n        fileManager: FileManagerKit,\n        encoder: ToucanEncoder,\n        decoder: ToucanDecoder,\n        logger: Logger = .subsystem(\"template-loader\")\n    ) {\n        self.locations = locations\n        self.extensions = extensions\n        self.fileManager = fileManager\n        self.encoder = encoder\n        self.decoder = decoder\n        self.logger = logger\n    }\n\n    func loadView(\n        at url: URL,\n        path: String,\n        isContentOverride: Bool = false\n    ) throws -> View {\n        let basePath =\n            path\n            .split(separator: \".\")\n            .dropLast()\n            .joined(separator: \".\")\n\n        let id =\n            if isContentOverride {\n                basePath\n                    .split(separator: \"/\")\n                    .last.map(String.init) ?? \"\"\n            }\n            else {\n                basePath.replacing(\"/\", with: \".\")\n            }\n\n        let contents = try String(\n            contentsOf: url.appendingPathIfPresent(path),\n            encoding: .utf8\n        )\n        return .init(\n            id: id,\n            path: path,\n            contents: contents\n        )\n    }\n\n    func loadTemplateMetadata(\n        at url: URL\n    ) throws(TemplateLoaderError) -> Template.Metadata {\n        do {\n            return try ObjectLoader(\n                url: url,\n                locations: fileManager.find(\n                    name: \"template\",\n                    extensions: [\"yaml\", \"yml\"],\n                    at: url\n                ),\n                encoder: encoder,\n                decoder: decoder\n            )\n            .load(Template.Metadata.self)\n        }\n        catch {\n            throw .init(type: \"\\(Template.Metadata.self)\", error: error)\n        }\n    }\n\n    ///\n    /// Loads and builds a `Template` by collecting assets and templates from predefined locations.\n    ///\n    /// - Returns: A fully constructed `Template` instance.\n    /// - Throws: An error if file discovery fails.\n    public func load() throws -> Template {\n        let assets = fileManager.find(\n            recursively: true,\n            at: locations.currentTemplateAssetsURL\n        )\n        let templates = fileManager.find(\n            extensions: extensions,\n            recursively: true,\n            at: locations.currentTemplateViewsURL\n        )\n\n        let assetOverrides = fileManager.find(\n            recursively: true,\n            at: locations.currentTemplateAssetOverridesURL\n        )\n\n        let templateOverrides = fileManager.find(\n            extensions: extensions,\n            recursively: true,\n            at: locations.currentTemplateViewsOverridesURL\n        )\n\n        let contentAssetOverrides = fileManager.find(\n            recursively: true,\n            at: locations.siteAssetsURL\n        )\n\n        let contentTemplateOverrides = fileManager.find(\n            extensions: extensions,\n            recursively: true,\n            at: locations.contentsURL\n        )\n\n        let metadata = try loadTemplateMetadata(\n            at: locations.currentTemplateURL\n        )\n\n        let template = try Template(\n            metadata: metadata,\n            components: .init(\n                assets: assets,\n                views: templates.map {\n                    try loadView(\n                        at: locations.currentTemplateViewsURL,\n                        path: $0\n                    )\n                }\n            ),\n            overrides: .init(\n                assets: assetOverrides,\n                views: templateOverrides.map {\n                    try loadView(\n                        at: locations.currentTemplateViewsOverridesURL,\n                        path: $0\n                    )\n                }\n            ),\n            content: .init(\n                assets: contentAssetOverrides,\n                views: contentTemplateOverrides.map {\n                    try loadView(\n                        at: locations.contentsURL,\n                        path: $0,\n                        isContentOverride: true\n                    )\n                }\n            )\n        )\n        return template\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/MarkdownParser.swift",
    "content": "//\n//  MarkdownParser.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 04. 17..\n//\n\nimport Logging\nimport ToucanCore\nimport ToucanSerialization\n\n/// A parser for Markdown content that extracts front matter metadata and body content.\n///\n/// Utilizes a configurable separator, a decoder conforming to `ToucanDecoder`, and a logger.\npublic struct MarkdownParser {\n\n    /// The string used to separate front matter from content in the markdown input.\n    var separator: String\n    /// A decoder used to parse front matter into a typed dictionary.\n    var decoder: ToucanDecoder\n    /// A logger used to emit parsing-related debug messages.\n    var logger: Logger\n\n    /// Creates a new `MarkdownParser` instance.\n    ///\n    /// - Parameters:\n    ///   - separator: A string that separates front matter from content. Defaults to `\"---\"`.\n    ///   - decoder: A decoder conforming to `ToucanDecoder` for parsing front matter.\n    ///   - logger: A logger instance for emitting debug information. Defaults to a subsystem logger.\n    public init(\n        separator: String = \"---\",\n        decoder: ToucanDecoder,\n        logger: Logger = .subsystem(\"markdown-parser\")\n    ) {\n        self.separator = separator\n        self.decoder = decoder\n        self.logger = logger\n    }\n\n    /// Removes the front matter section from the given markdown string.\n    ///\n    /// - Parameter markdown: The markdown string containing optional front matter.\n    /// - Returns: The markdown string without front matter.\n    func dropFrontMatter(\n        _ markdown: String\n    ) -> String {\n        if markdown.starts(with: separator) {\n            return\n                markdown\n                .split(separator: separator)\n                .dropFirst()\n                .joined(separator: separator)\n        }\n        return markdown\n    }\n\n    /// Parses the markdown string into front matter and body content.\n    ///\n    /// - Parameter markdown: A markdown string possibly containing front matter.\n    /// - Returns: A `Markdown` instance containing parsed front matter and content.\n    /// - Throws: An error if front matter decoding fails.\n    public func parse(\n        _ markdown: String\n    ) throws -> Markdown {\n        guard markdown.starts(with: separator) else {\n            logger.debug(\"The markdown string has no front matter.\")\n            return .init(\n                frontMatter: [:],\n                contents: markdown\n            )\n        }\n\n        let parts = markdown.split(\n            separator: separator,\n            maxSplits: 1,\n            omittingEmptySubsequences: true\n        )\n\n        let rawFrontMatter = String(parts.first ?? \"\")\n        let frontMatter = try decoder.decode(\n            [String: AnyCodable].self,\n            from: rawFrontMatter\n        )\n        return .init(\n            frontMatter: frontMatter,\n            contents: dropFrontMatter(markdown)\n                .trimmingCharacters(\n                    in: .whitespacesAndNewlines\n                )\n        )\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Models/BuildTargetSource.swift",
    "content": "//\n//  BuildTargetSource.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 01. 31..\n//\n\nimport struct Foundation.URL\n\n/// A complete in-memory representation of a content source bundle,\n/// including its configuration, content, pipelines, templates, and more.\n///\n/// Typically, this structure is built after parsing a content directory\n/// and used as input to render or transform content.\npublic struct BuildTargetSource {\n\n    /// The root location of the source on the filesystem.\n    public var locations: BuiltTargetSourceLocations\n\n    /// The target to use to build the site.\n    public var target: Target\n\n    /// Global configuration for the project, often loaded from `config.yml`.\n    public var config: Config\n\n    /// Site-wide settings, often defined in `site.yml`.\n    public var settings: Settings\n\n    /// List of content pipelines.\n    public var pipelines: [Pipeline]\n\n    /// Definitions for content types, typically used to classify and validate content entries.\n    public var types: [ContentType]\n\n    /// A list of raw content items parsed from the source directory.\n    public var rawContents: [RawContent]\n\n    /// A list of custom block directives used in Markdown rendering.\n    public var blocks: [Block]\n\n    /// Initializes a fully populated `BuildTargetSource` from its constituent components.\n    ///\n    /// - Parameters:\n    ///   - locations: Filesystem URLs of the source contents.\n    ///   - target: The target to use to build the site.\n    ///   - config: The main configuration for the site/project.\n    ///   - settings: Site-level metadata like title, language, etc.\n    ///   - pipelines: Any content transformation pipelines to apply.\n    ///   - types: Definitions for content types in this source.\n    ///   - rawContents: Parsed content entries from the source.\n    ///   - blockDirectives: Definitions of custom Markdown block directives.\n    public init(\n        locations: BuiltTargetSourceLocations,\n        target: Target = .standard,\n        config: Config = .defaults,\n        settings: Settings = .defaults,\n        pipelines: [Pipeline] = [],\n        types: [ContentType] = [],\n        rawContents: [RawContent] = [],\n        blockDirectives: [Block] = []\n    ) {\n        self.locations = locations\n        self.target = target\n        self.config = config\n        self.settings = settings\n        self.pipelines = pipelines\n        self.types = types\n        self.rawContents = rawContents\n        self.blocks = blockDirectives\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Models/BuiltTargetSourceLocations.swift",
    "content": "//\n//  BuiltTargetSourceLocations.swift\n//  Toucan\n//\n//  Created by Viasz-Kádi Ferenc on 2025. 03. 01..\n//\n\nimport struct Foundation.URL\nimport Logging\nimport ToucanCore\n\n/// A computed mapping of project-relative URLs based on the loaded configuration and project root.\npublic struct BuiltTargetSourceLocations {\n\n    /// The base URL of the source directory.\n    public var baseURL: URL\n    /// The URL where content files are located.\n    public var contentsURL: URL\n    /// The URL of the site settings configuration file.\n    public var siteSettingsURL: URL\n    /// The URL pointing to site-wide asset resources.\n    public var siteAssetsURL: URL\n    /// The URL containing content type definitions.\n    public var typesURL: URL\n    /// The URL containing block directive definitions.\n    public var blocksURL: URL\n    /// The URL pointing to the pipeline configuration files.\n    public var pipelinesURL: URL\n    /// The URL where template definitions are located.\n    public var templatesURL: URL\n    /// The URL of the currently active template.\n    public var currentTemplateURL: URL\n    /// The URL containing assets for the current template.\n    public var currentTemplateAssetsURL: URL\n    /// The URL pointing to views for the current template.\n    public var currentTemplateViewsURL: URL\n    /// The URL pointing to the override directory of the current template.\n    public var currentTemplateOverridesURL: URL\n    /// The URL for overridden assets in the current template.\n    public var currentTemplateAssetOverridesURL: URL\n    /// The URL for overridden views in the current template.\n    public var currentTemplateViewsOverridesURL: URL\n\n    /// Creates a new `BuiltTargetSourceLocations` instance by computing file paths based on the project configuration.\n    ///\n    /// - Parameters:\n    ///   - sourceURL: The base URL of the source directory.\n    ///   - config: The configuration object describing relative paths for various components.\n    public init(\n        sourceURL: URL,\n        config: Config\n    ) {\n        let base = sourceURL\n\n        let contents =\n            base\n            .appendingPathIfPresent(config.contents.path)\n\n        let settings =\n            base\n            .appendingPathIfPresent(config.site.settings.path)\n        let assets =\n            base\n            .appendingPathIfPresent(config.site.assets.path)\n\n        let types =\n            base\n            .appendingPathIfPresent(config.types.path)\n        let blocks =\n            base\n            .appendingPathIfPresent(config.blocks.path)\n        let pipelines =\n            base\n            .appendingPathIfPresent(config.pipelines.path)\n        let templates =\n            base\n            .appendingPathIfPresent(config.templates.location.path)\n\n        let currentTemplate =\n            templates\n            .appendingPathIfPresent(config.templates.current.path)\n        let currentTemplateAssets =\n            currentTemplate\n            .appendingPathIfPresent(config.templates.assets.path)\n        let currentTemplateViews =\n            currentTemplate\n            .appendingPathIfPresent(config.templates.views.path)\n\n        let currentTemplateOverrides =\n            templates\n            .appendingPathIfPresent(config.templates.overrides.path)\n            .appendingPathIfPresent(config.templates.current.path)\n        let currentTemplateAssetOverrides =\n            currentTemplateOverrides\n            .appendingPathIfPresent(config.templates.assets.path)\n        let currentTemplateViewsOverrides =\n            currentTemplateOverrides\n            .appendingPathIfPresent(config.templates.views.path)\n\n        self.baseURL = base\n        self.contentsURL = contents\n        self.siteSettingsURL = settings\n        self.siteAssetsURL = assets\n        self.typesURL = types\n        self.blocksURL = blocks\n        self.pipelinesURL = pipelines\n        self.templatesURL = templates\n        self.currentTemplateURL = currentTemplate\n        self.currentTemplateAssetsURL = currentTemplateAssets\n        self.currentTemplateViewsURL = currentTemplateViews\n        self.currentTemplateOverridesURL = currentTemplateOverrides\n        self.currentTemplateAssetOverridesURL = currentTemplateAssetOverrides\n        self.currentTemplateViewsOverridesURL = currentTemplateViewsOverrides\n    }\n}\n\nextension BuiltTargetSourceLocations: LoggerMetadataRepresentable {\n    /// This metadata can be used to provide additional context in log output.\n    public var logMetadata: [String: Logger.MetadataValue] {\n        [\n            \"baseUrl\": .string(baseURL.absoluteString)\n        ]\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Models/Markdown.swift",
    "content": "//\n//  Markdown.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 18..\n//\n\n/// A representation of a Markdown document that includes front matter metadata and raw content.\n///\n/// This model is useful for parsing, transforming, and rendering Markdown files.\npublic struct Markdown: Equatable {\n\n    /// A dictionary containing parsed front matter metadata.\n    ///\n    /// Typically includes key-value pairs defined at the top of the Markdown file (e.g., `title`, `author`, `date`).\n    public var frontMatter: [String: AnyCodable]\n\n    /// The body content of the Markdown file, excluding front matter.\n    public var contents: String\n\n    /// Initializes a new `Markdown` instance with front matter and Markdown content.\n    ///\n    /// - Parameters:\n    ///   - frontMatter: A dictionary of metadata parsed from the front matter section.\n    ///   - contents: The Markdown body as a string.\n    public init(\n        frontMatter: [String: AnyCodable] = [:],\n        contents: String = \"\"\n    ) {\n        self.frontMatter = frontMatter\n        self.contents = contents\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Models/Origin.swift",
    "content": "//\n//  Origin.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 01. 30..\n//\n\n/// Represents the source origin of a content item.\npublic struct Origin: Equatable {\n    /// The original path of the page bundle directory.\n    ///\n    /// This also acts as a unique identifier for the content within the file system.\n    public var path: Path\n\n    /// The slug, typically derived from the path and influenced by noindex files or directory structure.\n    ///\n    /// This slug is used to generate URLs, permalinks, or unique identifiers in the rendered site.\n    public var slug: String\n\n    /// Initializes a new `Origin` instance with the given path and slug.\n    ///\n    /// - Parameters:\n    ///   - path: The source directory of the content.\n    ///   - slug: The derived slug based on the path and metadata.\n    public init(\n        path: Path,\n        slug: String\n    ) {\n        self.path = path\n        self.slug = slug\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Models/Path.swift",
    "content": "//\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 representing a path for a raw content item.\npublic struct Path: Equatable {\n\n    /// The raw value as a string.\n    public var value: String\n\n    /// Initializes a new path.\n    ///\n    /// - Parameter value: The raw path value string.\n    public init(\n        _ value: String\n    ) {\n        self.value = value\n    }\n}\n\nextension Path: Codable {\n    /// Creates a new instance by decoding from the given decoder.\n    ///\n    /// This initializer attempts to decode the value as a single string.\n    ///\n    /// - Parameter decoder: The decoder to read data from.\n    /// - Throws: An error if reading from the decoder fails, or if the data is not a single string.\n    public init(\n        from decoder: Decoder\n    ) throws {\n        let container = try decoder.singleValueContainer()\n        self.value = try container.decode(String.self)\n    }\n\n    /// Encodes this value into the given encoder.\n    ///\n    /// This method encodes the value as a single string.\n    ///\n    /// - Parameter encoder: The encoder to write data to.\n    /// - Throws: An error if encoding fails.\n    public func encode(\n        to encoder: Encoder\n    ) throws {\n        var container = encoder.singleValueContainer()\n        try container.encode(value)\n    }\n}\n\npublic extension Path {\n    /// Returns a new `Path` instance with the last component removed.\n    ///\n    /// Useful for extracting the base directory of a given path.\n    ///\n    /// - Returns: A `Path` instance without the final path component.\n    func basePath() -> Path {\n        let rawPath =\n            value\n            .split(separator: \"/\")\n            .dropLast()\n            .joined(separator: \"/\")\n\n        return .init(rawPath)\n    }\n\n    /// Returns a string with all content inside brackets removed.\n    ///\n    /// Optionally removes percent encoding before processing.\n    ///\n    /// - Parameter shouldRemovePercentEncoding: A Boolean value that indicates whether to remove percent encoding.\n    /// - Returns: A string without the content inside square brackets.\n    func trimmingBracketsContent(\n        shouldRemovePercentEncoding: Bool = true\n    ) -> String {\n        var result = \"\"\n        var insideBrackets = false\n\n        let finalValue =\n            if shouldRemovePercentEncoding {\n                value.removingPercentEncoding ?? value\n            }\n            else {\n                value\n            }\n\n        for char in finalValue {\n            if char == \"[\" {\n                insideBrackets = true\n            }\n            else if char == \"]\" {\n                insideBrackets = false\n            }\n            else if !insideBrackets {\n                result.append(char)\n            }\n        }\n        return result\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Models/RawContent.swift",
    "content": "//\n//  RawContent.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 01. 30..\n//\n\n/// Represents the raw, unprocessed state of a content file, typically sourced from a page bundle.\n///\n/// Includes both the Markdown body and its front matter metadata, along with file origin and assets.\npublic struct RawContent: Equatable {\n    /// The origin of the content file, including its path and slug.\n    public var origin: Origin\n\n    /// The raw Markdown content body.\n    public var markdown: Markdown\n\n    /// The last modification timestamp (e.g., from file metadata), in Unix epoch format.\n    public var lastModificationDate: Double\n\n    /// The location of the assets folder relative from the origin path.\n    public var assetsPath: String\n\n    /// A list of asset paths associated with this content (e.g., images, attachments).\n    public var assets: [String]\n\n    /// Initializes a new `RawContent` instance.\n    ///\n    /// - Parameters:\n    ///   - origin: The origin information of the content file.\n    ///   - markdown: The contents using the `Markdown` type.\n    ///   - lastModificationDate: The file's last modification time (Unix timestamp).\n    ///   - assetsPath: The location of the assets folder relative from the origin path.\n    ///   - assets: List of asset file paths linked with this content.\n    public init(\n        origin: Origin,\n        markdown: Markdown = .init(),\n        lastModificationDate: Double,\n        assetsPath: String,\n        assets: [String]\n    ) {\n        self.origin = origin\n        self.markdown = markdown\n        self.lastModificationDate = lastModificationDate\n        self.assetsPath = assetsPath\n        self.assets = assets\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Models/Template.swift",
    "content": "//\n//  Template.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 21..\n//\n\nimport struct Foundation.URL\nimport Version\n\n/**\n Templates directory structure:\n\n ```\n templates\n    default\n        assets\n        views\n    overrides\n        default\n            assets\n            views\n ```\n */\n\n/// Represents a template used by the Toucan system, including paths to assets and templates for both base and override components.\npublic struct Template {\n\n    /// Metadata associated with the template, such as author, version, and tags.\n    public var metadata: Metadata\n    /// The primary components of the template.\n    public var components: Components\n    /// Override components that can replace or augment the default components.\n    public var overrides: Components\n    /// Content-specific components such as assets and templates used within the template.\n    public var content: Components\n\n    /// Creates a new instance.\n    ///\n    /// - Parameters:\n    ///   - metadata: Metadata associated with the template.\n    ///   - components: The primary components of the template.\n    ///   - overrides: Override components that can replace or augment the default components.\n    ///   - content: Content-specific components such as assets and templates used within the template.\n    public init(\n        metadata: Metadata,\n        components: Components,\n        overrides: Components,\n        content: Components\n    ) {\n        self.metadata = metadata\n        self.components = components\n        self.overrides = overrides\n        self.content = content\n    }\n}\n\npublic extension Template {\n\n    /// A group of assets and templates that make up a template component.\n    struct Components {\n\n        /// A list of asset file paths associated with the component.\n        public var assets: [String]\n        /// A list of templates associated with the component.\n        public var views: [View]\n\n        /// Creates a new `Components` instance.\n        ///\n        /// - Parameters:\n        ///   - assets: A list of asset file paths.\n        ///   - views: A list of views.\n        public init(\n            assets: [String],\n            views: [View]\n        ) {\n            self.assets = assets\n            self.views = views\n        }\n    }\n}\n\nextension Template {\n\n    /// Returns a dictionary of template IDs and their contents.\n    ///\n    /// - Returns: A dictionary where the keys are template IDs and the values are their contents.\n    public func getViewIDsWithContents() -> [String: String] {\n        let views = components.views + overrides.views + content.views\n        let result = views.reduce(into: [String: String]()) {\n            $0[$1.id] = $1.contents\n        }\n\n        return .init(uniqueKeysWithValues: result.sorted { $0.key < $1.key })\n    }\n}\n\npublic extension Template {\n\n    /// Metadata describing a template, such as name, version, license, and author.\n    struct Metadata: Codable {\n        /// The name of the template.\n        public var name: String\n        /// A short description of the template.\n        public var description: String\n        /// The URL where the template can be found or referenced.\n        public var url: String?\n        /// The version of the template.\n        public var version: String?\n        /// The versions of the generator this template is compatible with.\n        public var generatorVersion: GeneratorVersion\n        /// Licensing information for the template.\n        public var license: License?\n        /// Author information for the template.\n        public var authors: [Author]?\n        /// A demo link showing the template in action.\n        public var demo: Demo?\n        /// A list of tags to classify or describe the template.\n        public var tags: [String]\n    }\n}\n\npublic extension Template.Metadata {\n\n    struct GeneratorVersion: Codable, Sendable {\n\n        private enum CodingKeys: CodingKey, CaseIterable {\n            case value\n            case type\n        }\n\n        /// The base version value that the template supports.\n        public let value: Version\n\n        /// The version comparison method used during validation.\n        public let type: ComparisonType\n\n        /// Initializes a new instance with the specified version and comparison type.\n        /// - Parameters:\n        ///   - value: The version to be used for comparison.\n        ///   - type: The type of comparison to perform. Defaults to `.upNextMajor`.\n        public init(\n            value: Version,\n            type: ComparisonType = .upNextMajor\n        ) {\n            self.value = value\n            self.type = type\n        }\n\n        /// Initializes a new instance of the model from the given decoder.\n        /// - Parameter decoder: The decoder to read data from.\n        /// - Throws: An error if decoding fails or if unknown keys are present.\n        /// - Note: Validates unknown keys using `CodingKeys`. The `type` property is defaulting to `.upNextMajor` if not present.\n        public init(from decoder: any Decoder) throws {\n            try decoder.validateUnknownKeys(keyType: CodingKeys.self)\n\n            let container = try decoder.container(keyedBy: CodingKeys.self)\n\n            self.value = try container.decode(Version.self, forKey: .value)\n            self.type =\n                try container.decodeIfPresent(\n                    ComparisonType.self,\n                    forKey: .type\n                ) ?? .upNextMajor\n        }\n    }\n}\n\npublic extension Template.Metadata.GeneratorVersion {\n\n    enum ComparisonType: String, Codable, Sendable {\n        case upNextMajor\n        case upNextMinor\n        case exact\n    }\n}\n\npublic extension Template.Metadata {\n\n    /// Licensing details for the template.\n    struct License: Codable {\n        /// The name of the license.\n        let name: String\n        /// The URL to the license text or information.\n        let url: String?\n    }\n}\n\npublic extension Template.Metadata {\n\n    /// Author details for the template.\n    struct Author: Codable {\n        /// The author's name.\n        let name: String\n        /// A URL to the author's website or profile.\n        let url: String?\n    }\n}\n\npublic extension Template.Metadata {\n\n    /// Demo resource reference for the template.\n    struct Demo: Codable {\n        /// A URL to the live demo or preview of the template.\n        let url: String\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Models/View.swift",
    "content": "//\n//  View.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 01. 31..\n//\n\nimport Foundation\n\n/// Represents the physical location of a Mustache file, identified by a logical ID.\npublic struct View: Equatable {\n\n    /// A unique identifier for the template\n    public var id: String\n\n    /// The file system path to the template file relative from the selected template directory.\n    public var path: String\n\n    /// The contents of the template file.\n    public var contents: String\n\n    /// Creates a new template instance.\n    ///\n    /// - Parameters:\n    ///   - id: A unique identifier for the template.\n    ///   - path: The relative file system path of the template file.\n    ///   - contents: The full contents of the template file.\n    public init(\n        id: String,\n        path: String,\n        contents: String\n    ) {\n        self.id = id\n        self.path = path\n        self.contents = contents\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/AnyCodable.swift",
    "content": "//\n//  AnyCodable.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 04. 17..\n\nimport Foundation\n\n// public protocol AnySendable: Sendable {\n//\n// }\n\n/// A type-erased wrapper for any `Codable` value, allowing serialization of\n/// heterogeneous data structures (e.g., JSON-like dictionaries or YAML trees).\n///\n/// Supports dynamic type resolution during encoding/decoding,\n/// literal initialization, value extraction, and hashing.\npublic struct AnyCodable: Codable {\n    /// The wrapped value (may be `nil`, scalar, array, dictionary, etc.).\n    public var value: Any?\n\n    /// Initializes with any optional value.\n    public init(_ value: (some Any)?) {\n        self.value = value\n    }\n\n    /// Decodes a value from the given decoder and stores it in a type-erased wrapper.\n    ///\n    /// Automatically handles null, scalars, arrays, and dictionaries.\n    ///\n    /// - Parameter decoder: The decoder providing the data.\n    /// - Throws: `DecodingError.dataCorruptedError` if the value cannot be decoded.\n    public init(from decoder: Decoder) throws {\n        let container = try decoder.singleValueContainer()\n\n        if container.decodeNil() {\n            self.init(nil as Any?)\n        }\n        else if let bool = try? container.decode(Bool.self) {\n            self.init(bool)\n        }\n        else if let int = try? container.decode(Int.self) {\n            self.init(int)\n        }\n        else if let double = try? container.decode(Double.self) {\n            self.init(double)\n        }\n        else if let string = try? container.decode(String.self) {\n            self.init(string)\n        }\n        else if let array = try? container.decode([AnyCodable].self) {\n            self.init(array.map(\\.value))\n        }\n        else if let dictionary = try? container.decode(\n            [String: AnyCodable].self\n        ) {\n            self.init(dictionary.mapValues { $0 })\n        }\n        else {\n            throw DecodingError.dataCorruptedError(\n                in: container,\n                debugDescription: \"AnyCodable value cannot be decoded\"\n            )\n        }\n    }\n\n    /// Attempts to cast the internal value to a concrete type.\n    ///\n    /// - Parameter _: The target type.\n    /// - Returns: The casted value, or `nil` if the cast fails.\n    public func value<T>(as _: T.Type) -> T? {\n        value as? T\n    }\n\n    /// Encodes the wrapped value using the provided encoder.\n    ///\n    /// Supports scalars, arrays, dictionaries, and any `Encodable` object.\n    /// Throws an error for unsupported types.\n    ///\n    /// - Parameter encoder: The encoder to write data to.\n    /// - Throws: `EncodingError.invalidValue` if the value cannot be encoded.\n    public func encode(\n        to encoder: Encoder\n    ) throws {\n        var container = encoder.singleValueContainer()\n\n        switch value {\n        case nil:\n            try container.encodeNil()\n        case let bool as Bool:\n            try container.encode(bool)\n        case let int as Int:\n            try container.encode(int)\n        case let double as Double:\n            try container.encode(double)\n        case let string as String:\n            try container.encode(string)\n        case let array as [Any?]:\n            try container.encode(array.map { AnyCodable($0) })\n        case let dictionary as [String: Any?]:\n            try container.encode(dictionary.mapValues { AnyCodable($0) })\n        case let encodable as Encodable:\n            try encodable.encode(to: encoder)\n        case _ as NSNull:\n            try container.encodeNil()\n        default:\n            throw EncodingError.invalidValue(\n                value!,\n                .init(\n                    codingPath: container.codingPath,\n                    debugDescription: \"AnyCodable value cannot be encoded\"\n                )\n            )\n        }\n    }\n}\n\npublic extension AnyCodable {\n    func boolValue() -> Bool? { value(as: Bool.self) }\n    func intValue() -> Int? { value(as: Int.self) }\n    func doubleValue() -> Double? { value(as: Double.self) }\n    func stringValue() -> String? { value(as: String.self) }\n    func arrayValue<T>(as _: T.Type) -> [T] { value(as: [T].self) ?? [] }\n    func dictValue() -> [String: AnyCodable] {\n        value(as: [String: AnyCodable].self) ?? [:]\n    }\n}\n\nextension AnyCodable: Equatable {\n    /// Compares two `AnyCodable` values for equality, including nested structures.\n    ///\n    /// Only compares supported primitive and collection types (Bool, Int, Double, String,\n    /// arrays, and dictionaries). Other types will return `false`.\n    public static func == (lhs: AnyCodable, rhs: AnyCodable) -> Bool {\n        switch (lhs.value, rhs.value) {\n        case (nil, nil):\n            true\n        case let (lhs as Bool, rhs as Bool):\n            lhs == rhs\n        case let (lhs as Int, rhs as Int):\n            lhs == rhs\n        case let (lhs as Double, rhs as Double):\n            lhs == rhs\n        case let (lhs as String, rhs as String):\n            lhs == rhs\n        case let (lhs as [AnyCodable], rhs as [AnyCodable]):\n            lhs == rhs\n        case let (lhs as [String: AnyCodable], rhs as [String: AnyCodable]):\n            lhs == rhs\n        default:\n            false\n        }\n    }\n}\n\nextension AnyCodable: CustomStringConvertible {\n    /// Returns a human-readable description of the wrapped value.\n    ///\n    /// Falls back to `String(describing:)` if the value does not conform to `CustomStringConvertible`.\n    public var description: String {\n        switch value {\n        case let value as CustomStringConvertible:\n            value.description\n        default:\n            String(describing: value)\n        }\n    }\n}\n\nextension AnyCodable: CustomDebugStringConvertible {\n    /// Returns a debug-friendly string representation of the wrapped value.\n    ///\n    /// Prefixes the output with `\"AnyCodable(...)\"` for easy identification.\n    public var debugDescription: String {\n        switch value {\n        case let value as CustomDebugStringConvertible:\n            \"AnyCodable(\\(value.debugDescription))\"\n        default:\n            \"AnyCodable(\\(description))\"\n        }\n    }\n}\n\nextension AnyCodable: ExpressibleByNilLiteral {\n    /// Initializes an `AnyCodable` with `nil`.\n    public init(nilLiteral _: ()) {\n        self.init(nil as Any?)\n    }\n}\n\nextension AnyCodable: ExpressibleByBooleanLiteral {\n    /// Initializes an `AnyCodable` with a boolean literal.\n    public init(booleanLiteral value: Bool) {\n        self.init(value)\n    }\n}\n\nextension AnyCodable: ExpressibleByIntegerLiteral {\n    /// Initializes an `AnyCodable` with an integer literal.\n    public init(integerLiteral value: Int) {\n        self.init(value)\n    }\n}\n\nextension AnyCodable: ExpressibleByFloatLiteral {\n    /// Initializes an `AnyCodable` with a floating-point literal.\n    public init(floatLiteral value: Double) {\n        self.init(value)\n    }\n}\n\nextension AnyCodable: ExpressibleByStringLiteral {\n    /// Initializes an `AnyCodable` with a string literal.\n    public init(stringLiteral value: String) {\n        self.init(value)\n    }\n\n    /// Required for extended grapheme cluster literals.\n    public init(extendedGraphemeClusterLiteral value: String) {\n        self.init(value)\n    }\n}\n\nextension AnyCodable: ExpressibleByStringInterpolation {}\n\nextension AnyCodable: ExpressibleByArrayLiteral {\n    /// Initializes an `AnyCodable` with an array literal.\n    public init(arrayLiteral elements: Any...) {\n        self.init(elements)\n    }\n}\n\nextension AnyCodable: ExpressibleByDictionaryLiteral {\n    /// Initializes an `AnyCodable` with a dictionary literal.\n    ///\n    /// Also recursively wraps nested dictionaries and arrays.\n    public init(dictionaryLiteral elements: (AnyHashable, Any)...) {\n        var dict: [String: AnyCodable] = [:]\n        for (key, value) in elements {\n            let converted: AnyCodable\n            if let childDict = value as? [AnyHashable: Any] {\n                var newDict: [String: AnyCodable] = [:]\n                for (childKey, childValue) in childDict {\n                    newDict[String(describing: childKey)] = AnyCodable(\n                        childValue\n                    )\n                }\n                converted = AnyCodable(newDict)\n            }\n            else if let arrayValue = value as? [Any] {\n                let newArray = arrayValue.map { element -> AnyCodable in\n                    if let dictElement = element as? [AnyHashable: Any] {\n                        var newDict: [String: AnyCodable] = [:]\n                        for (childKey, childValue) in dictElement {\n                            newDict[String(describing: childKey)] = AnyCodable(\n                                childValue\n                            )\n                        }\n                        return AnyCodable(newDict)\n                    }\n                    return AnyCodable(element)\n                }\n                converted = AnyCodable(newArray)\n            }\n            else {\n                converted = AnyCodable(value)\n            }\n            dict[String(describing: key)] = converted\n        }\n        self.init(dict)\n    }\n}\n\nextension AnyCodable: Hashable {\n    /// Computes a hash based on the value type.\n    ///\n    /// Only values of supported types will be hashed.\n    public func hash(into hasher: inout Hasher) {\n        switch value {\n        case let value as Bool:\n            hasher.combine(value)\n        case let value as Int:\n            hasher.combine(value)\n        case let value as Double:\n            hasher.combine(value)\n        case let value as String:\n            hasher.combine(value)\n        case let value as [String: AnyCodable]:\n            hasher.combine(value)\n        case let value as [AnyCodable]:\n            hasher.combine(value)\n        default:\n            break  // Non-hashable values are ignored\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Blocks/Block+Attribute.swift",
    "content": "//\n//  Block+Attribute.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 18..\n//\n\npublic extension Block {\n    /// Represents a static HTML attribute that will be rendered on the directive's HTML tag.\n    struct Attribute: Sendable, Codable, Equatable {\n\n        /// The name of the HTML attribute (e.g., `class`, `id`).\n        public var name: String\n\n        /// The corresponding value of the attribute.\n        public var value: String\n\n        /// Initializes an `Attribute` for the rendered directive HTML tag.\n        ///\n        /// - Parameters:\n        ///   - name: The attribute key.\n        ///   - value: The attribute value.\n        public init(\n            name: String,\n            value: String\n        ) {\n            self.name = name\n            self.value = value\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Blocks/Block+Parameter.swift",
    "content": "//\n//  Block+Parameter.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 18..\n//\n\npublic extension Block {\n    /// Defines a configurable parameter for a directive, which may be required and have a default value.\n    struct Parameter: Sendable, Codable, Equatable {\n\n        /// The label of the parameter.\n        public var label: String\n\n        /// Indicates whether the parameter is required. Defaults to `nil` (optional).\n        public var isRequired: Bool?\n\n        /// A default value for the parameter, used if it is not explicitly specified in the directive.\n        public var defaultValue: String?\n\n        /// Initializes a `Parameter` for a directive.\n        ///\n        /// - Parameters:\n        ///   - label: The name of the parameter.\n        ///   - isRequired: Indicates if the parameter must be provided.\n        ///   - defaultValue: A fallback value if none is provided.\n        public init(\n            label: String,\n            isRequired: Bool? = nil,\n            defaultValue: String? = nil\n        ) {\n            self.label = label\n            self.isRequired = isRequired\n            self.defaultValue = defaultValue\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Blocks/Block.swift",
    "content": "//\n//  Block.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 17..\n//\n\n/// A representation of a custom block directive in Markdown, used for extending Markdown syntax with special tags or behaviors.\npublic struct Block: Sendable, Codable, Equatable {\n\n    /// The name of the directive.\n    public var name: String\n\n    /// A list of supported parameters for the directive.\n    public var parameters: [Parameter]?\n\n    /// If specified, this directive must appear within another directive of the given name.\n    public var requiresParentDirective: String?\n\n    /// Indicates whether child paragraphs should be removed from the HTML output. Defaults to `nil`.\n    public var removesChildParagraph: Bool?\n\n    /// The HTML tag to render (e.g., `\"div\"`, `\"section\"`).\n    public var tag: String?\n\n    /// Static attributes to apply to the rendered HTML tag.\n    public var attributes: [Attribute]?\n\n    /// Custom output HTML string that overrides default rendering behavior, if provided.\n    public var output: String?\n\n    /// Initializes a `MarkdownBlockDirective`.\n    ///\n    /// - Parameters:\n    ///   - name: The directive's name.\n    ///   - parameters: Optional list of accepted parameters.\n    ///   - requiresParentDirective: Name of a parent directive this one must reside within.\n    ///   - removesChildParagraph: Whether to exclude child `<p>` tags during rendering.\n    ///   - tag: HTML tag to be generated.\n    ///   - attributes: HTML attributes to apply.\n    ///   - output: Optional custom HTML output template.\n    public init(\n        name: String,\n        parameters: [Parameter]? = nil,\n        requiresParentDirective: String? = nil,\n        removesChildParagraph: Bool? = nil,\n        tag: String? = nil,\n        attributes: [Attribute]? = nil,\n        output: String? = nil\n    ) {\n        self.name = name\n        self.parameters = parameters\n        self.requiresParentDirective = requiresParentDirective\n        self.removesChildParagraph = removesChildParagraph\n        self.tag = tag\n        self.attributes = attributes\n        self.output = output\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Config/Config+Blocks.swift",
    "content": "//\n//  Config+Blocks.swift\n//  Toucan\n//\n//  Created by Viasz-Kádi Ferenc on 2025. 04. 18..\n//\n\npublic extension Config {\n    /// Represents the location of block configuration files.\n    struct Blocks: Codable, Equatable {\n\n        private enum CodingKeys: CodingKey, CaseIterable {\n            case path\n        }\n\n        /// Provides a default `Blocks` configuration pointing to `\"blocks\"`.\n        public static var defaults: Self {\n            .init(path: \"blocks\")\n        }\n\n        /// The relative or absolute path to the folder containing block configuration files.\n        ///\n        /// Example: `\"blocks\"` (default), or `\"config/blocks\"`\n        public var path: String\n\n        /// Initializes a new blocks configuration.\n        ///\n        /// - Parameter path: The directory where blocks configuration files are stored.\n        public init(path: String) {\n            self.path = path\n        }\n\n        /// Decodes the `Pipelines` configuration from a structured source.\n        ///\n        /// Falls back to `.defaults` if no container is available or the field is missing.\n        public init(\n            from decoder: any Decoder\n        ) throws {\n            try decoder.validateUnknownKeys(keyType: CodingKeys.self)\n\n            let defaults = Self.defaults\n            let container = try? decoder.container(keyedBy: CodingKeys.self)\n\n            guard let container else {\n                self = defaults\n                return\n            }\n\n            self.path =\n                try container.decodeIfPresent(String.self, forKey: .path)\n                ?? defaults.path\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Config/Config+Contents.swift",
    "content": "//\n//  Config+Contents.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 02. 21..\n//\n\npublic extension Config {\n    /// Defines file system paths for locating raw content and its associated assets.\n    struct Contents: Codable, Equatable {\n\n        private enum CodingKeys: CodingKey, CaseIterable {\n            case path\n            case assets\n        }\n\n        /// Provides a default content configuration using `contents` for source files\n        /// and `assets` for media or supporting files.\n        public static var defaults: Self {\n            .init(\n                path: \"contents\",\n                assets: .init(path: \"assets\")\n            )\n        }\n\n        /// The root directory path where raw content files (e.g., Markdown, YAML) are located.\n        ///\n        /// Example: `\"contents\"` or `\"src/content\"`\n        public var path: String\n\n        /// The location configuration for assets (e.g., images, attachments) linked to the content.\n        public var assets: Location\n\n        /// Initializes a custom `Contents` configuration.\n        ///\n        /// - Parameters:\n        ///   - path: The content folder path.\n        ///   - assets: The associated assets folder configuration.\n        public init(\n            path: String,\n            assets: Location\n        ) {\n            self.path = path\n            self.assets = assets\n        }\n\n        /// Decodes a `Contents` configuration from a serialized format.\n        ///\n        /// If values are missing, falls back to sensible defaults.\n        public init(\n            from decoder: any Decoder\n        ) throws {\n            try decoder.validateUnknownKeys(keyType: CodingKeys.self)\n\n            let defaults = Self.defaults\n\n            guard\n                let container = try? decoder.container(keyedBy: CodingKeys.self)\n            else {\n                self = defaults\n                return\n            }\n\n            self.path =\n                try container.decodeIfPresent(\n                    String.self,\n                    forKey: .path\n                ) ?? defaults.path\n\n            self.assets =\n                try container.decodeIfPresent(\n                    Location.self,\n                    forKey: .assets\n                ) ?? defaults.assets\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Config/Config+DataTypes+Date.swift",
    "content": "//\n//  Config+DataTypes+Date.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 02. 21..\n//\n\npublic extension Config.DataTypes {\n\n    /// Provides a configuration for parsing and formatting dates across the site or contents.\n    struct Date: Codable, Equatable {\n\n        private enum CodingKeys: CodingKey, CaseIterable {\n            case input\n            case output\n            case formats\n        }\n\n        /// Returns a default configuration using ISO 8601 parsing and no predefined output formats.\n        public static var defaults: Self {\n            .init(\n                input: .defaults,\n                output: .defaults,\n                formats: [:]\n            )\n        }\n\n        /// The expected format for parsing date input strings (typically from front matter or JSON).\n        ///\n        /// Example: `\"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\"` (ISO-8601 with milliseconds)\n        public var input: DateFormatterConfig\n\n        /// A custom date localization for the standard localized output formats.\n        public var output: DateLocalization\n\n        /// A dictionary of named output formats for rendering dates in different contexts.\n        ///\n        /// Example:\n        /// ```yaml\n        /// formats:\n        ///   short: { format: \"MMM d\" }\n        ///   full: { format: \"MMMM d, yyyy\" }\n        /// ```\n        public var formats: [String: DateFormatterConfig]\n\n        /// Initializes a custom date format configuration.\n        ///\n        /// - Parameters:\n        ///   - input: Format used to parse raw date values.\n        ///   - output: The date localization config for the standard date outputs.\n        ///   - formats: Named formats for rendering parsed dates.\n        public init(\n            input: DateFormatterConfig,\n            output: DateLocalization,\n            formats: [String: DateFormatterConfig]\n        ) {\n            self.input = input\n            self.output = output\n            self.formats = formats\n        }\n\n        /// Decodes the configuration from a serialized source,\n        /// applying default values for missing fields.\n        public init(\n            from decoder: any Decoder\n        ) throws {\n            try decoder.validateUnknownKeys(keyType: CodingKeys.self)\n\n            let defaults = Self.defaults\n\n            guard\n                let container = try? decoder.container(keyedBy: CodingKeys.self)\n            else {\n                self = defaults\n                return\n            }\n\n            self.input =\n                try container.decodeIfPresent(\n                    DateFormatterConfig.self,\n                    forKey: .input\n                ) ?? defaults.input\n\n            self.output =\n                try container.decodeIfPresent(\n                    DateLocalization.self,\n                    forKey: .output\n                ) ?? defaults.output\n\n            self.formats =\n                try container.decodeIfPresent(\n                    [String: DateFormatterConfig].self,\n                    forKey: .formats\n                ) ?? defaults.formats\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Config/Config+DataTypes.swift",
    "content": "//\n//  Config+DataTypes.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 02. 16..\n//\n\npublic extension Config {\n    /// Defines how core data types—like date formats—should be interpreted or rendered within a pipeline.\n    ///\n    /// `DataTypes` is a configuration layer that allows pipelines to specify\n    /// localized or project-specific formatting and handling logic for structured data.\n    struct DataTypes: Codable, Equatable {\n\n        private enum CodingKeys: CodingKey, CaseIterable {\n            case date\n        }\n\n        /// Returns the default `DataTypes` configuration, using `.defaults` for date formatting.\n        public static var defaults: Self {\n            .init(date: .defaults)\n        }\n\n        /// The configuration used to handle and format date values.\n        public var date: Date\n\n        /// Initializes a new `DataTypes` instance.\n        ///\n        /// - Parameter date: Date format configuration to apply.\n        public init(\n            date: Date\n        ) {\n            self.date = date\n        }\n\n        /// Decodes a `DataTypes` configuration from serialized input.\n        ///\n        /// Defaults to `.defaults` if the `date` field is missing.\n        ///\n        /// - Parameter decoder: The decoder to parse configuration from.\n        /// - Throws: A decoding error if any value is invalid.\n        public init(\n            from decoder: any Decoder\n        ) throws {\n            try decoder.validateUnknownKeys(keyType: CodingKeys.self)\n\n            let container = try decoder.container(keyedBy: CodingKeys.self)\n\n            let date =\n                try container.decodeIfPresent(\n                    Date.self,\n                    forKey: .date\n                ) ?? .defaults\n\n            self.init(date: date)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Config/Config+Location.swift",
    "content": "//\n//  Config+Location.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 02. 21..\n//\n\npublic extension Config {\n    /// Represents a named location within the file system.\n    struct Location: Codable, Equatable {\n\n        /// The file system path for this location (e.g., `\"assets\"`, `\"public/images\"`).\n        public var path: String\n\n        /// Initializes a new `Location` with a given path.\n        ///\n        /// - Parameter path: A relative or absolute path in the project.\n        public init(path: String) {\n            self.path = path\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Config/Config+Pipelines.swift",
    "content": "//\n//  Config+Pipelines.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 02. 21..\n//\n\npublic extension Config {\n    /// Represents the location of pipeline configuration files.\n    struct Pipelines: Codable, Equatable {\n\n        private enum CodingKeys: CodingKey, CaseIterable {\n            case path\n        }\n\n        /// Provides a default `Pipelines` configuration pointing to `\"pipelines\"`.\n        public static var defaults: Self {\n            .init(path: \"pipelines\")\n        }\n\n        /// The relative or absolute path to the folder containing pipeline configuration files.\n        ///\n        /// Example: `\"pipelines\"` (default), or `\"config/pipelines\"`\n        public var path: String\n\n        /// Initializes a new pipelines configuration.\n        ///\n        /// - Parameter path: The directory where pipeline configuration files are stored.\n        public init(path: String) {\n            self.path = path\n        }\n\n        /// Decodes the `Pipelines` configuration from a structured source.\n        ///\n        /// Falls back to `.defaults` if no container is available or the field is missing.\n        public init(\n            from decoder: any Decoder\n        ) throws {\n            try decoder.validateUnknownKeys(keyType: CodingKeys.self)\n\n            let defaults = Self.defaults\n            let container = try? decoder.container(keyedBy: CodingKeys.self)\n\n            guard let container else {\n                self = defaults\n                return\n            }\n\n            self.path =\n                try container.decodeIfPresent(String.self, forKey: .path)\n                ?? defaults.path\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Config/Config+Renderer+ParagraphStyles.swift",
    "content": "//\n//  Config+Renderer+ParagraphStyles.swift\n//  Toucan\n//\n//  Created by gerp83 on 2025. 04. 17..\n//\n\npublic extension Config.RendererConfig {\n    /// Defines paragraph style aliases for block-level directives\n    struct ParagraphStyles: Codable, Equatable {\n\n        /// Returns a standard `ParagraphStyles` configuration with common alias values.\n        public static var defaults: Self {\n            .init(\n                styles: [\n                    \"note\": [\"note\"],\n                    \"warning\": [\"warn\", \"warning\"],\n                    \"tip\": [\"tip\"],\n                    \"important\": [\"important\"],\n                    \"error\": [\"error\", \"caution\"],\n                ]\n            )\n        }\n\n        /// A dictionary mapping style group names to arrays of individual paragraph styles.\n        public var styles: [String: [String]]\n\n        /// Initializes a new object with custom style mappings.\n        ///\n        /// - Parameter styles: A style group representing the paragraph styles.\n        public init(\n            styles: [String: [String]]\n        ) {\n            self.styles = styles\n        }\n\n        /// Initializes a new instance by decoding from the given decoder.\n        ///\n        /// - Parameter decoder: The decoder to read data from.\n        /// - Throws: Only throws if the underlying decoding attempt throws unexpectedly;\n        ///           otherwise silently falls back to defaults.\n        public init(\n            from decoder: Decoder\n        ) throws {\n            guard\n                let container = try? decoder.singleValueContainer(),\n                let styles = try? container.decode([String: [String]].self)\n            else {\n                self.styles = Self.defaults.styles\n                return\n            }\n            self.styles = styles\n        }\n\n        /// Encodes this  instance into the given encoder.\n        ///\n        /// - Parameter encoder: The encoder to write data to.\n        /// - Throws: An error if any value is invalid for the given encoder’s format.\n        public func encode(\n            to encoder: Encoder\n        ) throws {\n            var container = encoder.singleValueContainer()\n            try container.encode(styles)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Config/Config+RendererConfig.swift",
    "content": "//\n//  Config+RendererConfig.swift\n//  Toucan\n//\n//  Created by gerp83 on 2025. 03. 28..\n//\n\npublic extension Config {\n    /// Defines default configurations used when rendering content,\n    /// including reading time settings, outline parsing depth, and\n    /// paragraph styling rules for directive blocks.\n    struct RendererConfig: Codable, Equatable {\n\n        private enum CodingKeys: CodingKey, CaseIterable {\n            case wordsPerMinute\n            case outlineLevels\n            case paragraphStyles\n        }\n\n        /// Returns a `ContentConfigurations` instance with sensible default values.\n        public static var defaults: Self {\n            .init(\n                wordsPerMinute: 238,\n                outlineLevels: [2, 3],\n                paragraphStyles: .defaults\n            )\n        }\n\n        /// The average reading speed used to estimate reading time (words per minute).\n        ///\n        /// Common default is 238 wpm, based on tested averages for fluent readers.\n        public var wordsPerMinute: Int\n\n        /// The heading levels to extract for outlines (e.g., `[2, 3]` means `##` and `###` in Markdown).\n        ///\n        /// These levels are used when generating tables of contents or section overviews.\n        public var outlineLevels: [Int]\n\n        /// Aliases for styled paragraph blocks (e.g., \"note\", \"tip\", \"error\").\n        public var paragraphStyles: ParagraphStyles\n\n        /// Initializes a custom `ContentConfigurations` instance.\n        ///\n        /// - Parameters:\n        ///   - wordsPerMinute: The average reading speed for estimating read time.\n        ///   - outlineLevels: Heading levels to extract for outline/toc generation.\n        ///   - paragraphStyles: Mappings for styled block directives.\n        public init(\n            wordsPerMinute: Int,\n            outlineLevels: [Int],\n            paragraphStyles: ParagraphStyles\n        ) {\n            self.wordsPerMinute = wordsPerMinute\n            self.outlineLevels = outlineLevels\n            self.paragraphStyles = paragraphStyles\n        }\n\n        /// Decodes a `ContentConfigurations` instance, applying defaults for missing fields.\n        ///\n        /// Gracefully falls back to `.defaults` if the decoding container is missing or incomplete.\n        public init(\n            from decoder: any Decoder\n        ) throws {\n            try decoder.validateUnknownKeys(keyType: CodingKeys.self)\n\n            let defaults = Self.defaults\n\n            guard\n                let container = try? decoder.container(keyedBy: CodingKeys.self)\n            else {\n                self = defaults\n                return\n            }\n\n            self.wordsPerMinute =\n                try container.decodeIfPresent(Int.self, forKey: .wordsPerMinute)\n                ?? defaults.wordsPerMinute\n\n            self.outlineLevels =\n                try container.decodeIfPresent(\n                    [Int].self,\n                    forKey: .outlineLevels\n                )\n                ?? defaults.outlineLevels\n\n            self.paragraphStyles =\n                try container.decodeIfPresent(\n                    ParagraphStyles.self,\n                    forKey: .paragraphStyles\n                )\n                ?? defaults.paragraphStyles\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Config/Config+Site.swift",
    "content": "//\n//  Config+Site.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 02. 21..\n//\n\npublic extension Config {\n    /// Defines file system paths for locating site related resources.\n    struct Site: Codable, Equatable {\n\n        private enum CodingKeys: CodingKey, CaseIterable {\n            case assets\n            case settings\n        }\n\n        /// Provides a default content configuration\n        public static var defaults: Self {\n            .init(\n                assets: .init(path: \"assets\"),\n                settings: .init(path: \"\")\n            )\n        }\n\n        /// The location of the global site assets.\n        public var assets: Location\n\n        /// The location of the site settings.\n        public var settings: Location\n\n        /// Initializes a custom `Site` configuration.\n        ///\n        /// - Parameters:\n        ///   - assets: The assets folder location.\n        ///   - settings: The settings (site.yml) file location.\n        public init(\n            assets: Location,\n            settings: Location\n        ) {\n            self.assets = assets\n            self.settings = settings\n        }\n\n        /// Decodes a `Site` configuration from a serialized format.\n        ///\n        /// If values are missing, falls back to default values.\n        public init(\n            from decoder: any Decoder\n        ) throws {\n            try decoder.validateUnknownKeys(keyType: CodingKeys.self)\n\n            let defaults = Self.defaults\n\n            guard\n                let container = try? decoder.container(\n                    keyedBy: CodingKeys.self\n                )\n            else {\n                self = defaults\n                return\n            }\n\n            self.assets =\n                try container.decodeIfPresent(\n                    Location.self,\n                    forKey: .assets\n                ) ?? defaults.assets\n\n            self.settings =\n                try container.decodeIfPresent(\n                    Location.self,\n                    forKey: .settings\n                ) ?? defaults.settings\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Config/Config+Templates.swift",
    "content": "//\n//  Config+Templates.swift\n//  Toucan\n//\n//  Created by Viasz-Kádi Ferenc on 2025. 03. 01..\n//\n\nimport Foundation\n\npublic extension Config {\n    /// Defines the structure and paths for working with templates in the system.\n    struct Templates: Codable, Equatable {\n\n        private enum CodingKeys: CodingKey, CaseIterable {\n            case location\n            case current\n            case assets\n            case views\n            case overrides\n        }\n\n        /// Returns the default template configuration with all folders under the `\"templates\"` base.\n        public static var defaults: Self {\n            .init(\n                location: .init(path: \"templates\"),\n                current: .init(path: \"default\"),\n                assets: .init(path: \"assets\"),\n                views: .init(path: \"views\"),\n                overrides: .init(path: \"overrides\")\n            )\n        }\n\n        /// The base folder where all templates are stored (e.g., `\"templates\"`).\n        public var location: Location\n\n        /// The subfolder or identifier of the currently selected template (e.g., `\"default\"`, `\"dark\"`).\n        public var current: Location\n\n        /// The path inside the template where static assets (e.g., CSS, JS, images) are stored.\n        public var assets: Location\n\n        /// The path to the folder containing template views (e.g., HTML or markup layouts).\n        public var views: Location\n\n        /// A folder for override files that replace core behavior or template (optional).\n        public var overrides: Location\n\n        /// Initializes a configuration.\n        ///\n        /// - Parameters:\n        ///   - location: The base path containing all template folders.\n        ///   - current: The name or path of the active template.\n        ///   - assets: Folder path for template assets.\n        ///   - views: Folder path for views.\n        ///   - overrides: Folder path for template overrides.\n        public init(\n            location: Location,\n            current: Location,\n            assets: Location,\n            views: Location,\n            overrides: Location\n        ) {\n            self.location = location\n            self.current = current\n            self.assets = assets\n            self.views = views\n            self.overrides = overrides\n        }\n\n        /// Decodes a configuration from serialized input, falling back to default values when missing.\n        public init(\n            from decoder: any Decoder\n        ) throws {\n            try decoder.validateUnknownKeys(keyType: CodingKeys.self)\n\n            let defaults = Self.defaults\n            let container = try? decoder.container(keyedBy: CodingKeys.self)\n\n            guard let container else {\n                self = defaults\n                return\n            }\n\n            self.location =\n                try container.decodeIfPresent(Location.self, forKey: .location)\n                ?? defaults.location\n\n            self.current =\n                try container.decodeIfPresent(Location.self, forKey: .current)\n                ?? defaults.current\n\n            self.assets =\n                try container.decodeIfPresent(Location.self, forKey: .assets)\n                ?? defaults.assets\n\n            self.views =\n                try container.decodeIfPresent(Location.self, forKey: .views)\n                ?? defaults.views\n\n            self.overrides =\n                try container.decodeIfPresent(Location.self, forKey: .overrides)\n                ?? defaults.overrides\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Config/Config+Types.swift",
    "content": "//\n//  Config+Types.swift\n//  Toucan\n//\n//  Created by Viasz-Kádi Ferenc on 2025. 04. 18..\n//\n\npublic extension Config {\n    /// Represents the location of type configuration files.\n    struct Types: Sendable, Codable, Equatable {\n\n        private enum CodingKeys: CodingKey, CaseIterable {\n            case path\n        }\n\n        /// Provides a default `Types` configuration pointing to `\"types\"`.\n        public static var defaults: Self {\n            .init(path: \"types\")\n        }\n\n        /// The relative or absolute path to the folder containing type configuration files.\n        ///\n        /// Example: `\"types\"` (default), or `\"config/types\"`\n        public var path: String\n\n        /// Initializes a new types configuration.\n        ///\n        /// - Parameter path: The directory where type configuration files are stored.\n        public init(\n            path: String\n        ) {\n            self.path = path\n        }\n\n        /// Decodes the `Types` configuration from a structured source.\n        ///\n        /// Falls back to `.defaults` if no container is available or the field is missing.\n        public init(\n            from decoder: any Decoder\n        ) throws {\n            try decoder.validateUnknownKeys(keyType: CodingKeys.self)\n\n            let defaults = Self.defaults\n            let container = try? decoder.container(keyedBy: CodingKeys.self)\n\n            guard let container else {\n                self = defaults\n                return\n            }\n\n            self.path =\n                try container.decodeIfPresent(String.self, forKey: .path)\n                ?? defaults.path\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Config/Config.swift",
    "content": "//\n//  Config.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 01. 29..\n//\n\nimport Foundation\n\n/// Represents the top-level configuration for a content rendering system.\npublic struct Config: Codable, Equatable {\n\n    private enum CodingKeys: CodingKey, CaseIterable {\n        case site\n        case pipelines\n        case contents\n        case types\n        case blocks\n        case templates\n        case dataTypes\n        case renderer\n    }\n\n    /// Provides a default `Config` instance using defaults from all subcomponents.\n    ///\n    /// This is used when configuration fields are missing or omitted.\n    public static var defaults: Self {\n        .init(\n            site: .defaults,\n            pipelines: .defaults,\n            contents: .defaults,\n            types: .defaults,\n            blocks: .defaults,\n            templates: .defaults,\n            dataTypes: .defaults,\n            renderer: .defaults\n        )\n    }\n\n    /// Global site configuration.\n    public var site: Site\n\n    /// Pipeline configuration used to transform and render content.\n    public var pipelines: Pipelines\n\n    /// Configuration for mapping and locating raw content files.\n    public var contents: Contents\n\n    /// The folder where type-specific templates or definitions reside.\n    public var types: Types\n\n    /// A folder for reusable UI block components (e.g., hero, footer, card).\n    public var blocks: Blocks\n\n    /// Template-related configuration, including layout templates and style resources.\n    public var templates: Templates\n\n    /// Global date format settings for rendering and parsing dates.\n    public var dataTypes: DataTypes\n\n    /// Additional content-specific overrides or configuration extensions.\n    public var renderer: RendererConfig\n\n    /// Initializes a full `Config` instance.\n    ///\n    /// - Parameters:\n    ///   - site: Site configuration.\n    ///   - pipelines: Pipeline configurations.\n    ///   - contents: Content mapping configuration.\n    ///   - types: Folder path for type definitions.\n    ///   - blocks: Folder path for reusable block templates.\n    ///   - templates: Template layout and styling definitions.\n    ///   - dataTypes: Data type related configurations.\n    ///   - renderer: Fine-grained control for specific content types.\n    public init(\n        site: Site = .defaults,\n        pipelines: Pipelines = .defaults,\n        contents: Contents = .defaults,\n        types: Types = .defaults,\n        blocks: Blocks = .defaults,\n        templates: Templates = .defaults,\n        dataTypes: DataTypes = .defaults,\n        renderer: RendererConfig = .defaults\n    ) {\n        self.site = site\n        self.pipelines = pipelines\n        self.contents = contents\n        self.types = types\n        self.blocks = blocks\n        self.templates = templates\n        self.dataTypes = dataTypes\n        self.renderer = renderer\n    }\n\n    /// Decodes the `Config` from a structured data source (e.g., YAML or JSON),\n    /// applying defaults to any missing fields for robust deserialization.\n    ///\n    /// - Parameter decoder: The decoder used to load configuration.\n    /// - Throws: A decoding error if required structures are malformed.\n    public init(\n        from decoder: any Decoder\n    ) throws {\n        try decoder.validateUnknownKeys(keyType: CodingKeys.self)\n\n        let defaults = Self.defaults\n        let container = try? decoder.container(keyedBy: CodingKeys.self)\n\n        guard let container else {\n            self = defaults\n            return\n        }\n\n        self.site =\n            try container.decodeIfPresent(\n                Site.self,\n                forKey: .site\n            ) ?? defaults.site\n\n        self.pipelines =\n            try container.decodeIfPresent(\n                Pipelines.self,\n                forKey: .pipelines\n            ) ?? defaults.pipelines\n\n        self.contents =\n            try container.decodeIfPresent(\n                Contents.self,\n                forKey: .contents\n            ) ?? defaults.contents\n\n        self.types =\n            try container.decodeIfPresent(\n                Types.self,\n                forKey: .types\n            ) ?? defaults.types\n\n        self.blocks =\n            try container.decodeIfPresent(\n                Blocks.self,\n                forKey: .blocks\n            ) ?? defaults.blocks\n\n        self.templates =\n            try container.decodeIfPresent(\n                Templates.self,\n                forKey: .templates\n            ) ?? defaults.templates\n\n        self.dataTypes =\n            try container.decodeIfPresent(\n                DataTypes.self,\n                forKey: .dataTypes\n            ) ?? defaults.dataTypes\n\n        self.renderer =\n            try container.decodeIfPresent(\n                RendererConfig.self,\n                forKey: .renderer\n            ) ?? defaults.renderer\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Date/DateFormatterConfig.swift",
    "content": "//\n//  DateFormatterConfig.swift\n//  Toucan\n//\n//  Created by Viasz-Kádi Ferenc on 2025. 03. 28..\n//\n\n/// A configuration for formatting dates.\n///\n/// This type holds both localization options and a format string, allowing\n/// dates to be formatted according to locale, time zone, and pattern.\npublic struct DateFormatterConfig: Sendable, Codable, Equatable {\n\n    /// The keys used for encoding and decoding top-level date formatter properties.\n    private enum CodingKeys: String, CodingKey, CaseIterable {\n        case format\n        // NOTE: Multiple types are parsed from the same container. The keys listed below help make validation easier. Refer to `DateLocalization` for a related implementation.\n        case locale\n        case timeZone\n    }\n\n    /// Returns a default configuration using ISO 8601 parsing and no predefined output formats.\n    public static var defaults: Self {\n        .init(\n            localization: .defaults,\n            format: \"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\"\n        )\n    }\n\n    /// The locale and time zone options to apply when formatting dates.\n    public var localization: DateLocalization\n\n    /// The date format string (e.g., `\"yyyy-MM-dd\"`, `\"MMMM d, yyyy\"`).\n    public var format: String\n\n    /// Creates a new date formatter options instance.\n    ///\n    /// - Parameters:\n    ///   - localization: The locale and time zone options to apply.\n    ///   - format: A date format string (for example, `\"yyyy-MM-dd\"` or `\"MMMM d, yyyy\"`).\n    public init(\n        localization: DateLocalization,\n        format: String\n    ) {\n        self.localization = localization\n        self.format = format\n    }\n\n    /// Initializes a new `DateFormatterOptions` by decoding from the given decoder.\n    ///\n    /// - Parameter decoder: The decoder to read data from.\n    /// - Throws: An error if reading from the decoder fails, or if the data is corrupted or invalid.\n    public init(\n        from decoder: any Decoder\n    ) throws {\n        try decoder.validateUnknownKeys(keyType: CodingKeys.self)\n\n        self.localization = try DateLocalization(from: decoder)\n\n        let container = try decoder.container(keyedBy: CodingKeys.self)\n        let format = try container.decode(String.self, forKey: .format)\n\n        guard !format.isEmpty else {\n            throw DecodingError.dataCorrupted(\n                .init(\n                    codingPath: container.codingPath,\n                    debugDescription: \"Empty date format value.\"\n                )\n            )\n        }\n        self.format = format\n    }\n\n    /// Encodes this `DateFormatterOptions` into the given encoder.\n    ///\n    /// - Parameter encoder: The encoder to write data to.\n    /// - Throws: An error if any values are invalid for the given encoder’s format.\n    public func encode(\n        to encoder: Encoder\n    ) throws {\n        var container = encoder.container(keyedBy: CodingKeys.self)\n\n        try localization.encode(to: encoder)\n        try container.encode(format, forKey: .format)\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Date/DateLocalization.swift",
    "content": "//\n//  DateLocalization.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 28..\n//\n\nimport struct Foundation.Locale\nimport struct Foundation.TimeZone\n\n/// A set of locale and time zone identifiers used when formatting dates.\n///\n/// This type holds the locale and time zone identifiers that will be used\n/// by a date formatter to localize its output.\npublic struct DateLocalization: Sendable, Codable, Equatable {\n\n    /// The keys used for encoding and decoding top-level date formatter properties.\n    enum CodingKeys: CodingKey, CaseIterable {\n        case locale\n        case timeZone\n        // NOTE: Multiple types are parsed from the same container. The keys listed below help make validation easier. Refer to `DateFormatterConfig` for a related implementation.\n        case format\n    }\n\n    /// The default date localization options using the system’s default locale\n    /// (`\"en-US\"`) and time zone (`\"GMT\"`).\n    public static var defaults: Self {\n        .init(\n            locale: \"en-US\",\n            timeZone: \"GMT\"\n        )\n    }\n\n    /// The locale identifier used for formatting (e.g., `\"en_US\"`, `\"fr_FR\"`).\n    /// If `nil`, the system’s default locale will be used.\n    public var locale: String\n\n    /// The time zone identifier (e.g., `\"UTC\"`, `\"Europe/Budapest\"`).\n    /// If `nil`, the system’s default time zone will be used.\n    public var timeZone: String\n\n    /// Creates a new date localization options instance.\n    ///\n    /// - Parameters:\n    ///   - locale: A locale identifier (for example, `\"en_US\"` or `\"fr_FR\"`).\n    ///   - timeZone: A time zone identifier (for example, `\"UTC\"` or `\"Europe/Budapest\"`).\n    public init(\n        locale: String,\n        timeZone: String\n    ) {\n        self.locale = locale\n        self.timeZone = timeZone\n    }\n\n    /// Creates a new instance by decoding from the given decoder.\n    ///\n    /// - Parameter decoder: The decoder to read data from.\n    /// - Throws: An error if decoding fails, or if the locale or time zone identifier is invalid.\n    public init(\n        from decoder: any Decoder\n    ) throws {\n        try decoder.validateUnknownKeys(keyType: CodingKeys.self)\n\n        let container = try decoder.container(keyedBy: CodingKeys.self)\n        let defaults = Self.defaults\n\n        let locale =\n            try container.decodeIfPresent(\n                String.self,\n                forKey: .locale\n            ) ?? defaults.locale\n\n        let timeZone =\n            try container.decodeIfPresent(\n                String.self,\n                forKey: .timeZone\n            ) ?? defaults.timeZone\n\n        let id = Locale.identifier(.icu, from: locale)\n        guard Locale.availableIdentifiers.contains(id) else {\n            throw DecodingError.dataCorrupted(\n                .init(\n                    codingPath: container.codingPath,\n                    debugDescription: \"Invalid locale identifier.\"\n                )\n            )\n        }\n\n        guard TimeZone(identifier: timeZone) != nil else {\n            throw DecodingError.dataCorrupted(\n                .init(\n                    codingPath: container.codingPath,\n                    debugDescription: \"Invalid time zone identifier.\"\n                )\n            )\n        }\n\n        self.locale = locale\n        self.timeZone = timeZone\n    }\n\n    /// Encodes this `DateFormatterOptions` into the given encoder.\n    ///\n    /// - Parameter encoder: The encoder to write data to.\n    /// - Throws: An error if any values are invalid for the given encoder’s format.\n    public func encode(\n        to encoder: any Encoder\n    ) throws {\n        var container = encoder.container(keyedBy: CodingKeys.self)\n\n        let defaults = DateLocalization.defaults\n\n        if locale != defaults.locale {\n            try container.encode(locale, forKey: .locale)\n        }\n        if timeZone != defaults.timeZone {\n            try container.encode(timeZone, forKey: .timeZone)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Pipeline/Pipeline+Assets.swift",
    "content": "//\n//  Pipeline+Assets.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 04. 19..\n//\n\npublic extension Pipeline {\n    /// Represents a collection of asset declarations used during content rendering.\n    ///\n    /// Assets include static files like JavaScript, CSS, and images that are attached\n    /// to the output content, either by setting paths, loading files, or parsing content.\n    struct Assets: Codable {\n\n        private enum CodingKeys: CodingKey, CaseIterable {\n            case behaviors\n            case properties\n        }\n\n        /// Describes the file location for the asset.\n        public struct Location: Codable {\n\n            /// An optional path to the asset file.\n            public var path: String?\n            /// The base name of the file (without extension).\n            public var name: String\n            /// The file extension (e.g., `\"css\"`, `\"js\"`).\n            public var ext: String\n\n            //\n\n            /// Initializes a new `Input` describing an asset file.\n            ///\n            /// - Parameters:\n            ///   - path: Optional path to the file.\n            ///   - name: The file name without extension.\n            ///   - ext: The file extension.\n            public init(\n                path: String? = nil,\n                name: String,\n                ext: String\n            ) {\n                self.path = path\n                self.name = name\n                self.ext = ext\n            }\n        }\n\n        /// Describes a transformation between two asset locations, typically used for converting input files to a desired output format.\n        ///\n        /// The `Behavior` struct is useful in defining how assets should be handled during processing, for example,\n        /// converting a SCSS file to a CSS file or minifying JavaScript.\n        ///\n        /// - Properties:\n        ///   - id: A unique identifier for the behavior.\n        ///   - input: The source location of the asset.\n        ///   - output: The destination location for the processed asset.\n        public struct Behavior: Codable {\n\n            private enum CodingKeys: CodingKey, CaseIterable {\n                case id\n                case input\n                case output\n            }\n\n            /// The unique identifier for the behavior.\n            public var id: String\n            /// The input location for the behavior.\n            public var input: Location\n            /// The output location for the behavior.\n            public var output: Location\n\n            /// Initializes a behavior\n            ///\n            /// - Properties:\n            ///   - id: A unique identifier for the behavior.\n            ///   - input: The source location of the asset.\n            ///   - output: The destination location for the processed asset.\n            public init(\n                id: String,\n                input: Location,\n                output: Location\n            ) {\n                self.id = id\n                self.input = input\n                self.output = output\n            }\n\n            /// Decodes a `Behavior` instance from a configuration source (e.g., JSON/YAML).\n            ///\n            /// Missing fields default\n            public init(\n                from decoder: any Decoder\n            ) throws {\n                try decoder.validateUnknownKeys(keyType: CodingKeys.self)\n\n                let container = try decoder.container(keyedBy: CodingKeys.self)\n\n                let id = try container.decode(String.self, forKey: .id)\n\n                let input =\n                    try container.decodeIfPresent(\n                        Location.self,\n                        forKey: .input\n                    )\n                    ?? .init(\n                        name: \"*\",\n                        ext: \"*\"\n                    )\n\n                let output =\n                    try container.decodeIfPresent(\n                        Location.self,\n                        forKey: .output\n                    )\n                    ?? .init(\n                        name: \"*\",\n                        ext: \"*\"\n                    )\n\n                self.init(\n                    id: id,\n                    input: input,\n                    output: output\n                )\n            }\n        }\n\n        /// Represents a single asset manipulation instruction within the `Assets` configuration.\n        public struct Property: Codable {\n\n            /// Defines how the asset should be applied or processed.\n            public enum Action: String, Codable {\n                /// Add the asset to an existing list or collection.\n                case add\n                /// Overwrite or explicitly set the asset value.\n                case set\n                /// Load the asset from a specified path or resource.\n                case load\n                /// Parse the asset, typically used for dynamic formats (e.g., JSON, YAML).\n                case parse\n            }\n\n            /// The action to perform for this asset.\n            public var action: Action\n            /// The logical asset key or category (e.g., `\"js\"`, `\"image\"`).\n            public var property: String\n            /// Indicates whether the path to the file should be automatically resolved.\n            public var resolvePath: Bool\n            /// Describes the input file for the asset.\n            public var input: Location\n\n            /// Initializes a new `Property` describing an asset manipulation.\n            ///\n            /// - Parameters:\n            ///   - action: The type of action to perform (e.g., `.set`, `.add`).\n            ///   - property: The logical key or name for the asset (e.g., `\"css\"`).\n            ///   - resolvePath: Whether to resolve the input file path dynamically.\n            ///   - input: The input file descriptor.\n            public init(\n                action: Action,\n                property: String,\n                resolvePath: Bool,\n                input: Location\n            ) {\n                self.action = action\n                self.property = property\n                self.resolvePath = resolvePath\n                self.input = input\n            }\n        }\n\n        /// Returns a default asset configuration commonly used for HTML pipelines.\n        public static var defaults: Self {\n            .init(\n                behaviors: [],\n                properties: []\n            )\n        }\n\n        /// A list of asset behaviors\n        public var behaviors: [Behavior]\n\n        /// A list of asset manipulation rules.\n        public var properties: [Property]\n\n        /// Initializes an `Assets` instance with a given set of properties.\n        ///\n        /// - Parameters:\n        ///   - behaviors: The array of asset behaviors.\n        ///   - properties: The array of asset properties to include.\n        public init(\n            behaviors: [Behavior] = [],\n            properties: [Property] = []\n        ) {\n            self.behaviors = behaviors\n            self.properties = properties\n        }\n\n        /// Decodes the `Assets` instance from a decoder, applying empty defaults if necessary.\n        ///\n        /// - Parameter decoder: The decoder to use for deserialization.\n        /// - Throws: An error if decoding fails.\n        public init(\n            from decoder: any Decoder\n        ) throws {\n            try decoder.validateUnknownKeys(keyType: CodingKeys.self)\n\n            let container = try decoder.container(keyedBy: CodingKeys.self)\n\n            let defaults = Self.defaults\n\n            let behaviors =\n                try container.decodeIfPresent(\n                    [Behavior].self,\n                    forKey: .behaviors\n                ) ?? defaults.behaviors\n\n            let properties =\n                try container.decodeIfPresent(\n                    [Property].self,\n                    forKey: .properties\n                ) ?? defaults.properties\n\n            self.init(\n                behaviors: behaviors,\n                properties: properties\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Pipeline/Pipeline+ContentTypes.swift",
    "content": "//\n//  Pipeline+ContentTypes.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 02. 03..\n//\n\npublic extension Pipeline {\n    /// Defines rules for selecting and filtering content types used in a pipeline.\n    ///\n    /// `ContentTypes` allows explicit inclusion or exclusion of types, as well as\n    /// optional tracking for last modification timestamps.\n    struct ContentTypes: Codable {\n\n        private enum CodingKeys: CodingKey, CaseIterable {\n            case include\n            case exclude\n            case lastUpdate\n            case filterRules\n        }\n\n        /// Default configuration with no filtering or update tracking.\n        public static var defaults: Self {\n            .init(\n                include: [],\n                exclude: [],\n                lastUpdate: [],\n                filterRules: [:]\n            )\n        }\n\n        /// A list of content types to explicitly include.\n        ///\n        /// If this list is empty, all content types are included unless excluded.\n        public var include: [String]\n\n        /// A list of content types to explicitly exclude.\n        ///\n        /// These override entries in `include` and are always filtered out.\n        public var exclude: [String]\n\n        /// A list of content types that should be tracked for last update timestamps.\n        public var lastUpdate: [String]\n\n        /// A mapping of content type keys to filtering conditions.\n        ///\n        /// Each key represents a content type (e.g., `\"post\"`, `\"author\"`), and its value\n        /// defines a condition that must be met for the content to be included in the pipeline.\n        /// This enables fine-grained control over which specific content items are published.\n        ///\n        /// If a content type is not listed in `filterRules`, it is not subject to condition-based filtering.\n        public var filterRules: [String: Condition]\n\n        /// Initializes a new `ContentTypes` filter configuration.\n        ///\n        /// - Parameters:\n        ///   - include: List of explicitly allowed content types.\n        ///   - exclude: List of content types to exclude from processing.\n        ///   - lastUpdate: List of content types to monitor for timestamp changes.\n        ///   - filterRules: Mapping of content type keys to conditions used to filter content items.\n        public init(\n            include: [String],\n            exclude: [String],\n            lastUpdate: [String],\n            filterRules: [String: Condition]\n        ) {\n            self.include = include\n            self.exclude = exclude\n            self.lastUpdate = lastUpdate\n            self.filterRules = filterRules\n        }\n\n        /// Decodes a `ContentTypes` instance from a configuration source (e.g., JSON/YAML).\n        ///\n        /// Missing fields default to empty arrays.\n        public init(\n            from decoder: any Decoder\n        ) throws {\n            try decoder.validateUnknownKeys(keyType: CodingKeys.self)\n\n            let container = try decoder.container(keyedBy: CodingKeys.self)\n\n            let include =\n                try container.decodeIfPresent([String].self, forKey: .include)\n                ?? []\n            let exclude =\n                try container.decodeIfPresent([String].self, forKey: .exclude)\n                ?? []\n            let lastUpdate =\n                try container.decodeIfPresent(\n                    [String].self,\n                    forKey: .lastUpdate\n                ) ?? []\n\n            let filterRules: [String: Condition] =\n                try container.decodeIfPresent(\n                    [String: Condition].self,\n                    forKey: .filterRules\n                ) ?? [:]\n\n            self.init(\n                include: include,\n                exclude: exclude,\n                lastUpdate: lastUpdate,\n                filterRules: filterRules\n            )\n        }\n\n        /// Determines whether a given content type should be processed based on inclusion and exclusion rules.\n        ///\n        /// - Parameter contentType: The content type key (e.g., `\"blog\"`, `\"author\"`).\n        /// - Returns: `true` if the content type is allowed, `false` otherwise.\n        public func isAllowed(contentType: String) -> Bool {\n            if exclude.contains(contentType) {\n                return false\n            }\n            if include.isEmpty {\n                return true\n            }\n            return include.contains(contentType)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Pipeline/Pipeline+DataTypes+Date.swift",
    "content": "//\n//  Pipeline+DataTypes+Date.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 30..\n//\n\npublic extension Pipeline.DataTypes {\n    /// Provides a configuration for parsing and formatting dates across the site or contents.\n    struct Date: Codable, Equatable {\n\n        private enum CodingKeys: CodingKey, CaseIterable {\n            case output\n            case formats\n        }\n\n        /// Returns a default configuration using ISO 8601 parsing and no predefined output formats.\n        public static var defaults: Self {\n            .init(\n                output: nil,\n                formats: [:]\n            )\n        }\n\n        /// A custom date localization for the standard localized output formats.\n        public var output: DateLocalization?\n\n        /// A dictionary of named output formats for rendering dates in different contexts.\n        ///\n        /// Example:\n        /// ```yaml\n        /// formats:\n        ///   short: { format: \"MMM d\" }\n        ///   full: { format: \"MMMM d, yyyy\" }\n        /// ```\n        public var formats: [String: DateFormatterConfig]\n\n        /// Initializes a custom date format configuration.\n        ///\n        /// - Parameters:\n        ///   - output: The date localization config for the standard date outputs.\n        ///   - formats: Named formats for rendering parsed dates.\n        public init(\n            output: DateLocalization?,\n            formats: [String: DateFormatterConfig]\n        ) {\n            self.output = output\n            self.formats = formats\n        }\n\n        /// Decodes the configuration from a serialized source,\n        /// applying default values for missing fields.\n        public init(\n            from decoder: any Decoder\n        ) throws {\n            try decoder.validateUnknownKeys(keyType: CodingKeys.self)\n\n            let defaults = Self.defaults\n\n            guard\n                let container = try? decoder.container(keyedBy: CodingKeys.self)\n            else {\n                self = defaults\n                return\n            }\n\n            self.output =\n                try container.decodeIfPresent(\n                    DateLocalization.self,\n                    forKey: .output\n                ) ?? defaults.output\n\n            self.formats =\n                try container.decodeIfPresent(\n                    [String: DateFormatterConfig].self,\n                    forKey: .formats\n                ) ?? defaults.formats\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Pipeline/Pipeline+DataTypes.swift",
    "content": "//\n//  Pipeline+DataTypes.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 30..\n//\n\npublic extension Pipeline {\n    /// Defines how core data types—like date formats—should be interpreted or rendered within a pipeline.\n    ///\n    /// `DataTypes` is a configuration layer that allows pipelines to specify\n    /// localized or project-specific formatting and handling logic for structured data.\n    struct DataTypes: Codable, Equatable {\n\n        private enum CodingKeys: CodingKey, CaseIterable {\n            case date\n        }\n\n        /// Returns the default `DataTypes` configuration, using `.defaults` for date formatting.\n        public static var defaults: Self {\n            .init(date: .defaults)\n        }\n\n        /// The configuration used to handle and format date values.\n        public var date: Date\n\n        /// Initializes a new `DataTypes` instance.\n        ///\n        /// - Parameter date: Date format configuration to apply.\n        public init(\n            date: Date\n        ) {\n            self.date = date\n        }\n\n        /// Decodes a `DataTypes` configuration from serialized input.\n        ///\n        /// Defaults to `.defaults` if the `date` field is missing.\n        ///\n        /// - Parameter decoder: The decoder to parse configuration from.\n        /// - Throws: A decoding error if any value is invalid.\n        public init(\n            from decoder: any Decoder\n        ) throws {\n            try decoder.validateUnknownKeys(keyType: CodingKeys.self)\n\n            let container = try decoder.container(keyedBy: CodingKeys.self)\n\n            let date =\n                try container.decodeIfPresent(\n                    Date.self,\n                    forKey: .date\n                ) ?? .defaults\n\n            self.init(date: date)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Pipeline/Pipeline+Engine.swift",
    "content": "//\n//  Pipeline+Engine.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 02. 03..\n//\n\npublic extension Pipeline {\n    /// Represents the rendering engine configuration used in a content pipeline.\n    struct Engine: Codable {\n\n        private enum CodingKeys: CodingKey, CaseIterable {\n            case id\n            case options\n        }\n\n        /// A unique identifier for the engine (e.g., `\"html\"`, `\"api\"`, `\"rss\"`).\n        public var id: String\n\n        /// A map of engine-specific configuration options.\n        ///\n        /// These options are engine-dependent and may define things like layout names,\n        /// file extensions, or custom behaviors.\n        public var options: [String: AnyCodable]\n\n        /// Initializes a new engine configuration.\n        ///\n        /// - Parameters:\n        ///   - id: The unique identifier of the engine type.\n        ///   - options: A dictionary of custom configuration options.\n        public init(\n            id: String,\n            options: [String: AnyCodable] = [:]\n        ) {\n            self.id = id\n            self.options = options\n        }\n\n        /// Decodes an `Engine` instance from a configuration source.\n        ///\n        /// If `options` is not defined, it defaults to an empty dictionary.\n        ///\n        /// - Throws: A decoding error if required fields are missing or malformed.\n        public init(\n            from decoder: any Decoder\n        ) throws {\n            try decoder.validateUnknownKeys(keyType: CodingKeys.self)\n\n            let container = try decoder.container(keyedBy: CodingKeys.self)\n\n            let id = try container.decode(String.self, forKey: .id)\n\n            let options =\n                try container.decodeIfPresent(\n                    [String: AnyCodable].self,\n                    forKey: .options\n                ) ?? [:]\n\n            self.init(id: id, options: options)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Pipeline/Pipeline+Output.swift",
    "content": "//\n//  Pipeline+Output.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 02. 16..\n//\n\npublic extension Pipeline {\n    /// Describes the output configuration for a content pipeline.\n    struct Output: Codable {\n\n        private enum CodingKeys: CodingKey, CaseIterable {\n            case path\n            case file\n            case ext\n        }\n\n        /// The directory path where the output file should be written.\n        ///\n        /// This is relative to the site's output root (e.g., `\"public/blog\"`).\n        public var path: String\n\n        /// The base file name of the output file (without extension).\n        ///\n        /// Common values include `\"index\"`, `\"feed\"`, etc.\n        public var file: String\n\n        /// The file extension of the output file (e.g., `\"html\"`, `\"json\"`, `\"xml\"`).\n        public var ext: String\n\n        /// Initializes a new `Output` configuration.\n        ///\n        /// - Parameters:\n        ///   - path: The relative path to the output directory.\n        ///   - file: The base file name (e.g., `\"index\"`).\n        ///   - ext: The file extension (e.g., `\"html\"`).\n        public init(\n            path: String,\n            file: String,\n            ext: String\n        ) {\n            self.path = path\n            self.file = file\n            self.ext = ext\n        }\n\n        /// Decodes the `Output` configuration from a serialized format (e.g., JSON/YAML).\n        ///\n        /// - Throws: A decoding error if any required key is missing.\n        public init(\n            from decoder: any Decoder\n        ) throws {\n            try decoder.validateUnknownKeys(keyType: CodingKeys.self)\n\n            let container = try decoder.container(keyedBy: CodingKeys.self)\n\n            let path = try container.decode(String.self, forKey: .path)\n            let file = try container.decode(String.self, forKey: .file)\n            let ext = try container.decode(String.self, forKey: .ext)\n\n            self.init(\n                path: path,\n                file: file,\n                ext: ext\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Pipeline/Pipeline+Scope+Context.swift",
    "content": "//\n//  Pipeline+Scope+Context.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 02. 03..\n//\n\npublic extension Pipeline.Scope {\n    /// Represents the available data context for a rendering `Scope`.\n    struct Context: OptionSet, Codable {\n\n        /// Includes user-defined metadata and settings.\n        public static var userDefined: Self { .init(rawValue: 1 << 0) }\n\n        /// Includes all standard content properties (e.g., title, date).\n        public static var properties: Self { .init(rawValue: 1 << 1) }\n\n        /// Includes nested or inline contents (e.g., included markdown).\n        public static var contents: Self { .init(rawValue: 1 << 2) }\n\n        /// Includes resolved relations (e.g., related posts, authors).\n        public static var relations: Self { .init(rawValue: 1 << 3) }\n\n        /// Includes output from named or inline queries.\n        public static var queries: Self { .init(rawValue: 1 << 4) }\n\n        /// A context optimized for minimal, linked summaries.\n        public static var reference: Self {\n            [\n                .userDefined,\n                .properties,\n                .relations,\n                .contents,\n                .queries,\n            ]\n        }\n\n        /// A context optimized for list or collection rendering.\n        public static var list: Self {\n            [\n                .userDefined,\n                .properties,\n                .relations,\n                .contents,\n                .queries,\n            ]\n        }\n\n        /// A context optimized for detailed full-page rendering.\n        public static var detail: Self {\n            [\n                .userDefined,\n                .properties,\n                .relations,\n                .contents,\n                .queries,\n            ]\n        }\n\n        /// The underlying raw bitmask value used to represent the context.\n        public let rawValue: UInt\n\n        /// Returns the mapping of context options to their string names.\n        private var allOptions: [(Context, String)] {\n            [\n                (.userDefined, Keys.userDefined.rawValue),\n                (.properties, Keys.properties.rawValue),\n                (.contents, Keys.contents.rawValue),\n                (.relations, Keys.relations.rawValue),\n                (.queries, Keys.queries.rawValue),\n                (.reference, Keys.reference.rawValue),\n                (.list, Keys.list.rawValue),\n                (.detail, Keys.detail.rawValue),\n            ]\n        }\n\n        /// Returns the string names of the options contained in the context.\n        public var stringValues: [String] {\n            allOptions.compactMap { contains($0.0) ? $0.1 : nil }\n        }\n\n        /// Initializes the context using a raw value.\n        ///\n        /// - Parameter rawValue: The UInt representation of the context.\n        public init(rawValue: UInt) {\n            self.rawValue = rawValue\n        }\n\n        /// Initializes the context using a string name (e.g., \"properties\", \"detail\").\n        ///\n        /// - Parameter stringValue: The string representation of the context.\n        public init(stringValue: String) {\n            switch stringValue.lowercased() {\n            case Keys.userDefined.rawValue:\n                self = .userDefined\n            case Keys.properties.rawValue:\n                self = .properties\n            case Keys.contents.rawValue:\n                self = .contents\n            case Keys.relations.rawValue:\n                self = .relations\n            case Keys.queries.rawValue:\n                self = .queries\n            case Keys.reference.rawValue:\n                self = .reference\n            case Keys.list.rawValue:\n                self = .list\n            case Keys.detail.rawValue:\n                self = .detail\n            default:\n                self = []\n            }\n        }\n\n        /// Decodes the context from either a single string or an array of strings.\n        ///\n        /// Supports user-friendly formats like:\n        /// ```yaml\n        /// context: \"detail\"\n        /// ```\n        /// or\n        /// ```yaml\n        /// context: [\"properties\", \"relations\"]\n        /// ```\n        ///\n        /// - Parameter decoder: The decoder to use.\n        /// - Throws: A decoding error if format is not supported.\n        public init(\n            from decoder: any Decoder\n        ) throws {\n            let container = try decoder.singleValueContainer()\n            if let stringValue = try? container.decode(String.self) {\n                self.init(stringValue: stringValue)\n            }\n            else if let stringArray = try? container.decode([String].self) {\n                self = stringArray.reduce(into: []) {\n                    $0.insert(.init(stringValue: $1))\n                }\n            }\n            else {\n                throw DecodingError.dataCorruptedError(\n                    in: container,\n                    debugDescription: \"Invalid context format.\"\n                )\n            }\n        }\n\n        /// Encodes the context as a string or array of strings using the defined string values.\n        ///\n        /// - Parameter encoder: The encoder to write data to.\n        /// - Throws: An error if any values are invalid or encoding fails.\n        public func encode(\n            to encoder: any Encoder\n        ) throws {\n            var container = encoder.singleValueContainer()\n\n            if let matched = allOptions.first(where: { self == $0.0 }) {\n                try container.encode(matched.1)\n            }\n            else {\n                let parts = allOptions.filter { contains($0.0) }.map(\\.1)\n                try container.encode(parts)\n            }\n        }\n    }\n}\n\nextension Pipeline.Scope.Context {\n\n    /// String keys used to identify pipeline scope contexts.\n    public enum Keys: String, CaseIterable {\n        case userDefined\n        case properties\n        case contents\n        case relations\n        case queries\n        case reference\n        case list\n        case detail\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Pipeline/Pipeline+Scope.swift",
    "content": "//\n//  Pipeline+Scope.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 02. 03..\n//\n\npublic extension Pipeline {\n    /// Describes a rendering scope within a content pipeline.\n    struct Scope: Codable {\n\n        private enum CodingKeys: CodingKey, CaseIterable {\n            case id\n            case context\n            case fields\n        }\n\n        /// String keys used to identify pipeline scopes.\n        public enum Keys: String, CaseIterable {\n            case reference\n            case list\n            case detail\n            case wildcard = \"*\"\n        }\n\n        /// A scope for rendering lightweight summaries or IDs for use in references.\n        public static var reference: Scope {\n            .init(context: .reference)\n        }\n\n        /// A scope for rendering content in a list format (e.g., previews, teasers).\n        public static var list: Scope {\n            .init(context: .list)\n        }\n\n        /// A scope for rendering full content in detail pages.\n        public static var detail: Scope {\n            .init(context: .detail)\n        }\n\n        /// A standard mapping of common context names to their default scopes.\n        public static var standard: [String: Scope] {\n            [\n                Keys.reference.rawValue: reference,\n                Keys.list.rawValue: list,\n                Keys.detail.rawValue: detail,\n            ]\n        }\n\n        /// The default fallback scope set, applied to all content types via the `*` wildcard.\n        public static var `default`: [String: [String: Scope]] {\n            [\n                Keys.wildcard.rawValue: standard\n            ]\n        }\n\n        /// The rendering context this scope applies to (e.g., `.detail`, `.list`, `.reference`).\n        public var context: Context\n\n        /// The specific content fields to include when rendering in this scope.\n        /// If empty, all fields may be included by default.\n        public var fields: [String]\n\n        /// Initializes a `Scope` with a given context and set of fields.\n        ///\n        /// - Parameters:\n        ///   - context: The rendering context.\n        ///   - fields: The fields to expose in this scope.\n        public init(\n            context: Context = .detail,\n            fields: [String] = []\n        ) {\n            self.context = context\n            self.fields = fields\n        }\n\n        /// Decodes a `Scope` from configuration data, with fallback defaults.\n        ///\n        /// If `context` is not specified, defaults to `.detail`.\n        /// If `fields` are not specified, defaults to an empty list.\n        public init(\n            from decoder: any Decoder\n        ) throws {\n            try decoder.validateUnknownKeys(keyType: CodingKeys.self)\n\n            let container = try decoder.container(keyedBy: CodingKeys.self)\n\n            let context =\n                try container.decodeIfPresent(Context.self, forKey: .context)\n                ?? .detail\n            let fields =\n                try container.decodeIfPresent([String].self, forKey: .fields)\n                ?? []\n\n            self.init(context: context, fields: fields)\n        }\n\n        /// Encodes this `Scope` instance into the given encoder.\n        ///\n        /// This method encodes the `context` and `fields` properties using keyed encoding.\n        ///\n        /// - Parameter encoder: The encoder to write data to.\n        /// - Throws: An error if any values are invalid for the encoder’s format.\n        public func encode(\n            to encoder: any Encoder\n        ) throws {\n            var container = encoder.container(keyedBy: CodingKeys.self)\n            try container.encode(context, forKey: .context)\n            try container.encode(fields, forKey: .fields)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Pipeline/Pipeline+Transformers+Transformer.swift",
    "content": "//\n//  Pipeline+Transformers+Transformer.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 16..\n//\n\nimport Foundation\n\npublic extension Pipeline.Transformers {\n    /// Represents a content transformer command used in a transformation pipeline.\n    struct Transformer: Codable {\n        /// Coding keys for decoding path and name properties.\n        private enum CodingKeys: String, CodingKey, CaseIterable {\n            case path\n            case name\n        }\n\n        /// The directory path where the executable is located.\n        /// Defaults to `\"/usr/local/bin\"` if not explicitly specified.\n        public var path: String\n\n        /// The name of the executable or script to run.\n        public var name: String\n\n        /// Initializes a new `ContentTransformer` with an optional path and required name.\n        ///\n        /// - Parameters:\n        ///   - path: The directory path to the executable. Defaults to `\"/usr/local/bin\"`.\n        ///   - name: The name of the command-line executable or script.\n        public init(\n            path: String = \"/usr/local/bin\",\n            name: String\n        ) {\n            self.path = path\n            self.name = name\n        }\n\n        /// Decodes a `ContentTransformer` from a decoder, falling back to default path if missing.\n        ///\n        /// - Throws: A decoding error if the required `name` is not present.\n        public init(\n            from decoder: any Decoder\n        ) throws {\n            try decoder.validateUnknownKeys(keyType: CodingKeys.self)\n\n            let container = try decoder.container(keyedBy: CodingKeys.self)\n            self.path =\n                (try? container.decode(String.self, forKey: .path))\n                ?? \"/usr/local/bin\"\n            self.name = try container.decode(String.self, forKey: .name)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Pipeline/Pipeline+Transformers.swift",
    "content": "//\n//  Pipeline+Transformers.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 02. 21..\n//\n\npublic extension Pipeline {\n    /// Represents a sequence of content transformers to run before rendering,\n    /// along with an indicator of whether the final result is Markdown.\n    struct Transformers: Codable {\n\n        /// An ordered list of transformers (external commands or scripts) to execute.\n        ///\n        /// Each `ContentTransformer` represents an individual transformation step.\n        public var run: [Transformer]\n\n        /// Indicates whether the final output from this pipeline is expected to be Markdown.\n        ///\n        /// If `false`, the renderer may treat the output as already-formatted HTML or another format.\n        public var isMarkdownResult: Bool\n\n        /// Initializes a new `TransformerPipeline`.\n        ///\n        /// - Parameters:\n        ///   - run: An array of `ContentTransformer` instances to execute.\n        ///   - isMarkdownResult: A flag indicating whether the final output is Markdown. Defaults to `true`.\n        public init(\n            run: [Transformer] = [],\n            isMarkdownResult: Bool = true\n        ) {\n            self.run = run\n            self.isMarkdownResult = isMarkdownResult\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Pipeline/Pipeline.swift",
    "content": "//\n//  Pipeline.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 01. 16..\n//\n\n/// Represents a full content transformation pipeline,\n/// including scopes, queries, content types, engines, and outputs.\n///\n/// A pipeline defines how data flows from content source to final rendered output.\npublic struct Pipeline: Codable {\n\n    private enum CodingKeys: CodingKey, CaseIterable {\n        case id\n        case definesType\n        case scopes\n        case queries\n        case dataTypes\n        case contentTypes\n        case iterators\n        case assets\n        case transformers\n        case engine\n        case output\n    }\n\n    /// Unique identifier for the pipeline.\n    public var id: String\n\n    /// A Boolean value indicating whether the pipeline defines a virual type.\n    public var definesType: Bool\n\n    /// A nested map of content type → scope key → scope definition.\n    ///\n    /// This allows for per-content-type rendering rules (e.g., `detail`, `list`, `reference`).\n    public var scopes: [String: [String: Scope]]\n\n    /// Named query definitions that can be reused in scopes or iterators.\n    public var queries: [String: Query]\n\n    /// Definitions for global or scoped data types (e.g., formats, types).\n    public var dataTypes: DataTypes\n\n    /// Definitions for all known content types in the system.\n    public var contentTypes: ContentTypes\n\n    /// Static and external assets (e.g., JavaScript, CSS, images) used in rendering.\n    public var assets: Assets\n\n    /// Special iterator queries used for generating repeated content structures (e.g., pages in a list).\n    public var iterators: [String: Query]\n\n    /// Optional transformation pipelines, applied before rendering.\n    public var transformers: [String: Transformers]\n\n    /// The rendering engine to use (e.g., HTML, JSON, RSS).\n    public var engine: Engine\n\n    /// Output configuration for file generation and routing.\n    public var output: Output\n\n    /// Initializes a fully-defined `Pipeline` object.\n    public init(\n        id: String,\n        definesType: Bool = false,\n        scopes: [String: [String: Scope]] = [:],\n        queries: [String: Query] = [:],\n        dataTypes: DataTypes = .defaults,\n        contentTypes: ContentTypes = .defaults,\n        iterators: [String: Query] = [:],\n        assets: Assets = .defaults,\n        transformers: [String: Transformers] = [:],\n        engine: Engine,\n        output: Output\n    ) {\n        self.id = id\n        self.definesType = definesType\n        self.scopes = scopes\n        self.queries = queries\n        self.dataTypes = dataTypes\n        self.contentTypes = contentTypes\n        self.iterators = iterators\n        self.assets = assets\n        self.transformers = transformers\n        self.engine = engine\n        self.output = output\n    }\n\n    /// Decodes a pipeline from configuration, merging with defaults where applicable.\n    ///\n    /// Uses `Scope.default` as the baseline for scope resolution.\n    public init(\n        from decoder: any Decoder\n    ) throws {\n        try decoder.validateUnknownKeys(keyType: CodingKeys.self)\n\n        let container = try decoder.container(keyedBy: CodingKeys.self)\n\n        let id = try container.decode(String.self, forKey: .id)\n        let definesType =\n            try container.decodeIfPresent(Bool.self, forKey: .definesType)\n            ?? false\n\n        let defaultScopes = Scope.default\n        let userScopes =\n            try container.decodeIfPresent(\n                [String: [String: Scope]].self,\n                forKey: .scopes\n            ) ?? [:]\n        let scopes = defaultScopes.recursivelyMerged(with: userScopes)\n\n        let queries =\n            try container.decodeIfPresent(\n                [String: Query].self,\n                forKey: .queries\n            ) ?? [:]\n\n        let dataTypes =\n            try container.decodeIfPresent(\n                DataTypes.self,\n                forKey: .dataTypes\n            ) ?? .defaults\n\n        let contentTypes =\n            try container.decodeIfPresent(\n                ContentTypes.self,\n                forKey: .contentTypes\n            ) ?? .defaults\n\n        let iterators =\n            try container.decodeIfPresent(\n                [String: Query].self,\n                forKey: .iterators\n            ) ?? [:]\n\n        let assets =\n            try container.decodeIfPresent(\n                Assets.self,\n                forKey: .assets\n            ) ?? .defaults\n\n        let transformers =\n            try container.decodeIfPresent(\n                [String: Transformers].self,\n                forKey: .transformers\n            ) ?? [:]\n\n        let engine = try container.decode(Engine.self, forKey: .engine)\n        let output = try container.decode(Output.self, forKey: .output)\n\n        self.init(\n            id: id,\n            definesType: definesType,\n            scopes: scopes,\n            queries: queries,\n            dataTypes: dataTypes,\n            contentTypes: contentTypes,\n            iterators: iterators,\n            assets: assets,\n            transformers: transformers,\n            engine: engine,\n            output: output\n        )\n    }\n\n    /// Returns all scopes for a given content type.\n    ///\n    /// If no direct match is found, falls back to the wildcard `*` scopes.\n    ///\n    /// - Parameter contentType: The content type key (e.g., `\"post\"`).\n    /// - Returns: A map of scope keys (e.g., `\"list\"`, `\"detail\"`) to `Scope` values.\n    public func getScopes(\n        for contentType: String\n    ) -> [String: Scope] {\n        if let scopes = scopes[contentType] {\n            return scopes\n        }\n        return scopes[Scope.Keys.wildcard.rawValue] ?? [:]\n    }\n\n    /// Returns a single scope for a given content type and scope key (e.g., `\"list\"`, `\"detail\"`).\n    ///\n    /// Defaults to `.detail` if no specific match is found.\n    ///\n    /// - Parameters:\n    ///   - key: The scope key (e.g., `\"detail\"`, `\"reference\"`).\n    ///   - contentType: The content type key.\n    /// - Returns: A `Scope` object.\n    public func getScope(\n        keyedBy key: String,\n        for contentType: String\n    ) -> Scope {\n        let scopes = getScopes(for: contentType)\n        return scopes[key] ?? .detail\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Property/Property.swift",
    "content": "//\n//  Property.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 01. 21..\n//\n\n/// Represents a single content property definition, including its type,\n/// whether it's required, and an optional default value.\npublic struct Property: Codable, Equatable {\n\n    /// Coding keys used for decoding optional metadata fields.\n    enum CodingKeys: String, CodingKey, CaseIterable {\n        case type\n        case required\n        case defaultValue\n        // NOTE: Multiple types are parsed from the same container. The keys listed below help make validation easier. Refer to `PropertyType` for a related implementation.\n        case config\n    }\n\n    /// The type of the property (e.g., string, number, boolean, etc.).\n    public var type: PropertyType\n\n    /// Whether the property is required in the content entry.\n    ///\n    /// Defaults to `true` if not explicitly provided in the definition.\n    public var required: Bool\n\n    /// An optional default value to use if the property is missing in the content.\n    public var defaultValue: AnyCodable?\n\n    /// Initializes a new `Property` definition.\n    ///\n    /// - Parameters:\n    ///   - propertyType: The declared type of the property.\n    ///   - isRequired: Whether the field must be present in content. Defaults to `true` if not specified during decoding.\n    ///   - defaultValue: An optional default value to use if the content omits this property.\n    public init(\n        propertyType: PropertyType,\n        isRequired: Bool,\n        defaultValue: AnyCodable? = nil\n    ) {\n        self.type = propertyType\n        self.required = isRequired\n        self.defaultValue = defaultValue\n    }\n\n    /// Decodes a `Property` from a serialized representation, handling both the\n    /// core type and optional metadata (required flag and default value).\n    ///\n    /// If the `required` field is missing, it defaults to `true`.\n    public init(\n        from decoder: any Decoder\n    ) throws {\n        try decoder.validateUnknownKeys(keyType: CodingKeys.self)\n\n        let type = try decoder.singleValueContainer().decode(PropertyType.self)\n\n        let container = try decoder.container(keyedBy: CodingKeys.self)\n\n        let required =\n            try container.decodeIfPresent(Bool.self, forKey: .required) ?? true\n\n        let anyValue = try container.decodeIfPresent(\n            AnyCodable.self,\n            forKey: .defaultValue\n        )\n\n        self.init(\n            propertyType: type,\n            isRequired: required,\n            defaultValue: anyValue\n        )\n    }\n\n    /// Encodes the `Property` into a keyed container\n    ///\n    /// - Parameter encoder: The encoder to write data to.\n    /// - Throws: An error if encoding fails.\n    public func encode(\n        to encoder: any Encoder\n    ) throws {\n        var container = encoder.container(keyedBy: CodingKeys.self)\n        try type.encode(to: encoder)\n        try container.encode(required, forKey: .required)\n        try container.encodeIfPresent(self.defaultValue, forKey: .defaultValue)\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Property/PropertyType.swift",
    "content": "//\n//  PropertyType.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 01. 21..\n//\n\n/// Represents the type of a content property.\n///\n/// Used in defining content schemas or type-safe metadata fields.\n/// Supports primitive types (`bool`, `int`, `double`, `string`, `date`)\n/// and complex structures like arrays of types.\npublic indirect enum PropertyType: Sendable, Codable, Equatable {\n    /// Boolean type (`true` or `false`).\n    case bool\n\n    /// Integer type (`Int`).\n    case int\n\n    /// Floating-point number type (`Double`).\n    case double\n\n    /// Text/string type (`String`).\n    case string\n\n    /// Asset reference stored as a string value\n    case asset\n\n    /// Date type with optional localized formatting.\n    case date(config: DateFormatterConfig?)\n\n    /// Array type with elements of a consistent `PropertyType`.\n    case array(of: PropertyType)\n\n    /// Coding keys used for encoding and decoding `PropertyType`.\n    private enum CodingKeys: String, CodingKey {\n        case type\n        // date input config\n        case config\n        // type of array elements\n        case of\n    }\n\n    /// Type discriminator used during encoding and decoding.\n    private enum TypeKey: String, Sendable, Codable, Equatable, CaseIterable {\n        case bool\n        case int\n        case double\n        case string\n        case asset\n        case date\n        case array\n    }\n\n    /// Creates a new instance by decoding from the given decoder.\n    ///\n    /// Supports primitive and nested types with optional date formatting.\n    ///\n    /// - Parameter decoder: The decoder to read data from.\n    /// - Throws: An error if decoding fails.\n    public init(\n        from decoder: Decoder\n    ) throws {\n        let container = try decoder.container(keyedBy: CodingKeys.self)\n        let type = try container.decode(TypeKey.self, forKey: .type)\n\n        switch type {\n        case .bool:\n            self = .bool\n        case .int:\n            self = .int\n        case .double:\n            self = .double\n        case .string:\n            self = .string\n        case .asset:\n            self = .asset\n        case .date:\n            let config = try container.decodeIfPresent(\n                DateFormatterConfig.self,\n                forKey: .config\n            )\n            self = .date(config: config)\n        case .array:\n            let itemType = try container.decode(PropertyType.self, forKey: .of)\n            self = .array(of: itemType)\n        }\n    }\n\n    /// Encodes this value into the given encoder.\n    ///\n    /// - Parameter encoder: The encoder to write data to.\n    /// - Throws: An error if encoding fails.\n    public func encode(\n        to encoder: Encoder\n    ) throws {\n        var container = encoder.container(keyedBy: CodingKeys.self)\n\n        switch self {\n        case .bool:\n            try container.encode(TypeKey.bool, forKey: .type)\n        case .int:\n            try container.encode(TypeKey.int, forKey: .type)\n        case .double:\n            try container.encode(TypeKey.double, forKey: .type)\n        case .string:\n            try container.encode(TypeKey.string, forKey: .type)\n        case .asset:\n            try container.encode(TypeKey.asset, forKey: .type)\n        case let .date(config):\n            try container.encode(TypeKey.date, forKey: .type)\n            try container.encodeIfPresent(config, forKey: .config)\n        case let .array(of):\n            try container.encode(TypeKey.array, forKey: .type)\n            try container.encode(of, forKey: .of)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Property/SystemPropertyKeys.swift",
    "content": "//\n//  SystemPropertyKeys.swift\n//  Toucan\n//\n//  Created by Ferenc Viasz-Kadi on 2025. 08. 22..\n//\n\n/// Represents predefined system property keys used throughout Toucan.\npublic enum SystemPropertyKeys: String, CaseIterable {\n    /// Unique identifier for the object.\n    case id\n    /// Timestamp indicating the last modification date of the object.\n    case lastUpdate\n    /// URL-friendly identifier (slug) for the object.\n    case slug\n    /// The type or category of the object.\n    case type\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Query/Condition.swift",
    "content": "//\n//  Condition.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 01. 21..\n//\n\n/// Represents a logical condition used to filter content during a query.\n///\n/// `Condition` supports both field-based comparisons and compound logic (AND/OR),\n/// and can be resolved dynamically with parameters at runtime.\npublic enum Condition: Codable, Equatable {\n    /// A condition that compares a content field to a value using an operator.\n    case field(key: String, operator: Operator, value: AnyCodable)\n\n    /// A logical AND of multiple conditions (all must be true).\n    case and([Condition])\n\n    /// A logical OR of multiple conditions (at least one must be true).\n    case or([Condition])\n\n    /// Internal keys used for encoding and decoding `Condition` enum cases.\n    private enum CodingKeys: CodingKey, CaseIterable {\n        case key\n        case `operator`\n        case value\n        case and\n        case or\n    }\n\n    /// Decodes a `Condition` from a decoder, supporting `.field`, `.and`, and `.or` branches.\n    ///\n    /// Throws a decoding error if none of the known variants are valid in the input.\n    public init(\n        from decoder: any Decoder\n    ) throws {\n        try decoder.validateUnknownKeys(keyType: CodingKeys.self)\n\n        let container = try decoder.container(keyedBy: CodingKeys.self)\n\n        if let key = try? container.decode(String.self, forKey: .key),\n            let op = try? container.decode(Operator.self, forKey: .operator),\n            let anyValue = try? container.decode(\n                AnyCodable.self,\n                forKey: .value\n            )\n        {\n            self = .field(key: key, operator: op, value: anyValue)\n        }\n        else if let values = try? container.decode(\n            [Condition].self,\n            forKey: .and\n        ) {\n            self = .and(values)\n        }\n        else if let values = try? container.decode(\n            [Condition].self,\n            forKey: .or\n        ) {\n            self = .or(values)\n        }\n        else {\n            throw DecodingError.dataCorrupted(\n                .init(\n                    codingPath: decoder.codingPath,\n                    debugDescription: \"Invalid data for the Condition type.\"\n                )\n            )\n        }\n    }\n\n    /// Encodes this `Condition` instance into the given encoder.\n    ///\n    /// - Parameter encoder: The encoder to write data to.\n    /// - Throws: An error if encoding fails.\n    public func encode(\n        to encoder: Encoder\n    ) throws {\n        var container = encoder.container(keyedBy: CodingKeys.self)\n        switch self {\n        case let .field(key, op, value):\n            try container.encode(key, forKey: .key)\n            try container.encode(op, forKey: .operator)\n            try container.encode(value, forKey: .value)\n        case let .and(conditions):\n            try container.encode(conditions, forKey: .and)\n        case let .or(conditions):\n            try container.encode(conditions, forKey: .or)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Query/Direction.swift",
    "content": "//\n//  Direction.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 01. 21..\n//\n\n/// Represents the direction for sorting query results: ascending or descending.\npublic enum Direction: String, Sendable, Codable, Equatable, CaseIterable {\n    /// Sort in ascending order (e.g., A–Z, 1–9).\n    case asc\n\n    /// Sort in descending order (e.g., Z–A, 9–1).\n    case desc\n\n    /// The default sorting direction. Defaults to `.asc`.\n    public static var defaults: Self {\n        .asc\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Query/Operator.swift",
    "content": "//\n//  Operator.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 01. 21..\n//\n\n/// Represents a comparison or filtering operator used in queries.\npublic enum Operator: String, Sendable, Codable, Equatable, CaseIterable {\n    // bool, int, double, string\n    case equals\n\n    // bool, int, double, string\n    case notEquals\n\n    // int, double\n    case lessThan\n\n    // int, double\n    case lessThanOrEquals\n\n    // int, double\n    case greaterThan\n\n    // int, double\n    case greaterThanOrEquals\n\n    // string\n    case like\n\n    // string\n    case caseInsensitiveLike\n\n    // field is a single value check is in array of values\n    // array of int, double, string\n    case `in`\n\n    // field is an array check contains single value\n    // single value int, double, string\n    case contains\n\n    // field is an array check intersection with array value\n    // array values both int, double, string\n    case matching\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Query/Order.swift",
    "content": "//\n//  Order.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 01. 15..\n//\n\n/// Represents a sorting rule for ordering content query results.\n///\n/// Each `Order` defines a content field to sort by and the direction of sorting.\npublic struct Order: Sendable, Codable, Equatable {\n\n    /// Internal keys used for encoding and decoding `Order` instances.\n    /// Keys used for decoding an `Order` from external sources (e.g., YAML, JSON).\n    enum CodingKeys: CodingKey, CaseIterable {\n        case key\n        case direction\n    }\n\n    /// The name of the field to sort by (e.g., `\"date\"`, `\"title\"`, `\"priority\"`).\n    public var key: String\n\n    /// The direction to sort the field (`asc` or `desc`).\n    public var direction: Direction\n\n    /// Creates a new `Order` instance.\n    ///\n    /// - Parameters:\n    ///   - key: The field name to sort by.\n    ///   - direction: The sorting direction. Defaults to `.asc`.\n    public init(\n        key: String,\n        direction: Direction = .asc\n    ) {\n        self.key = key\n        self.direction = direction\n    }\n\n    /// Decodes an `Order` from a decoder.\n    ///\n    /// If the `direction` field is missing, it defaults to `.asc`.\n    public init(\n        from decoder: any Decoder\n    ) throws {\n        try decoder.validateUnknownKeys(keyType: CodingKeys.self)\n\n        let container = try decoder.container(keyedBy: CodingKeys.self)\n        let key = try container.decode(String.self, forKey: .key)\n        let direction =\n            try container.decodeIfPresent(Direction.self, forKey: .direction)\n            ?? .defaults\n\n        self.init(\n            key: key,\n            direction: direction\n        )\n    }\n\n    /// Encodes this `Order` instance into the given encoder.\n    ///\n    /// - Parameter encoder: The encoder to write data to.\n    /// - Throws: An error if encoding fails.\n    public func encode(\n        to encoder: any Encoder\n    ) throws {\n        var container = encoder.container(keyedBy: CodingKeys.self)\n        try container.encode(key, forKey: .key)\n        try container.encode(direction, forKey: .direction)\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Query/Query.swift",
    "content": "//\n//  Query.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 01. 15..\n//\n\n/// Represents a content query used to fetch or filter content entries\n/// based on content type, pagination, sorting, and filtering criteria.\npublic struct Query: Codable, Equatable {\n\n    /// Keys used to decode the query from a structured format like YAML or JSON.\n    enum CodingKeys: String, CodingKey, CaseIterable {\n        case contentType\n        case scope\n        case limit\n        case offset\n        case filter\n        case orderBy\n    }\n\n    /// The content type this query targets (e.g., `\"blog\"`, `\"author\"`, `\"product\"`).\n    public var contentType: String\n\n    /// An optional named scope to apply custom context (e.g., `\"homepage\"`, `\"featured\"`).\n    public var scope: String?\n\n    /// Optional limit for how many items to return.\n    public var limit: Int?\n\n    /// Optional offset for pagination, defining how many items to skip.\n    public var offset: Int?\n\n    /// An optional filter condition to narrow results (e.g., field comparison, boolean logic).\n    public var filter: Condition?\n\n    /// A list of fields and directions for ordering results.\n    public var orderBy: [Order]\n\n    /// Initializes a `Query` with specified properties.\n    ///\n    /// - Parameters:\n    ///   - contentType: The name of the content type being queried.\n    ///   - scope: An optional named context or scope for this query.\n    ///   - limit: The number of results to limit to.\n    ///   - offset: The number of results to skip (for pagination).\n    ///   - filter: A filter condition to apply to the results.\n    ///   - orderBy: Sorting rules for the query results.\n    public init(\n        contentType: String,\n        scope: String? = nil,\n        limit: Int? = nil,\n        offset: Int? = nil,\n        filter: Condition? = nil,\n        orderBy: [Order] = []\n    ) {\n        self.contentType = contentType\n        self.scope = scope\n        self.limit = limit\n        self.offset = offset\n        self.filter = filter\n        self.orderBy = orderBy\n    }\n\n    /// Decodes a `Query` instance from a decoder, applying defaults for optional values.\n    public init(\n        from decoder: any Decoder\n    ) throws {\n        try decoder.validateUnknownKeys(keyType: CodingKeys.self)\n\n        let container = try decoder.container(keyedBy: CodingKeys.self)\n\n        let contentType = try container.decode(\n            String.self,\n            forKey: .contentType\n        )\n        let scope = try container.decodeIfPresent(String.self, forKey: .scope)\n        let limit = try container.decodeIfPresent(Int.self, forKey: .limit)\n        let offset = try container.decodeIfPresent(Int.self, forKey: .offset)\n        let filter = try container.decodeIfPresent(\n            Condition.self,\n            forKey: .filter\n        )\n        let orderBy =\n            try container.decodeIfPresent([Order].self, forKey: .orderBy) ?? []\n\n        self.init(\n            contentType: contentType,\n            scope: scope,\n            limit: limit,\n            offset: offset,\n            filter: filter,\n            orderBy: orderBy\n        )\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Relation/Relation.swift",
    "content": "//\n//  Relation.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 01. 21..\n//\n\n/// Represents a relationship between a content item and one or more other content items.\n///\n/// A `Relation` defines how content items are connected, such as linking a blog post\n/// to its author or related articles. It includes the type of relation,\n/// reference key(s), and optional ordering rules.\npublic struct Relation: Codable, Equatable {\n\n    /// Keys used to decode the relation from serialized formats like JSON or YAML.\n    enum CodingKeys: CodingKey, CaseIterable {\n        case references\n        case type\n        case order\n    }\n\n    /// The key or query string that identifies the related content.\n    ///\n    /// This might represent a single ID, a tag filter, or a content type to resolve.\n    public var references: String\n\n    /// The type of relation, describing how the content is linked (e.g., one-to-one, many-to-one).\n    public var type: RelationType\n\n    /// Optional sorting logic to apply to related content (e.g., by date or title).\n    public var order: Order?\n\n    /// Creates a new `Relation` instance with required and optional properties.\n    ///\n    /// - Parameters:\n    ///   - references: A string identifying the target or criteria for the relation.\n    ///   - relationType: The relation type (e.g., `.single`, `.collection`).\n    ///   - order: Optional sorting rules for related content.\n    public init(\n        references: String,\n        relationType: RelationType,\n        order: Order? = nil\n    ) {\n        self.references = references\n        self.type = relationType\n        self.order = order\n    }\n\n    /// Decodes a `Relation` from a decoder, applying custom key mapping and optional logic.\n    ///\n    /// This ensures that all fields are safely extracted and defaults applied if necessary.\n    public init(\n        from decoder: any Decoder\n    ) throws {\n        try decoder.validateUnknownKeys(keyType: CodingKeys.self)\n\n        let container = try decoder.container(keyedBy: CodingKeys.self)\n\n        let references = try container.decode(String.self, forKey: .references)\n        let type = try container.decode(RelationType.self, forKey: .type)\n        let order = try container.decodeIfPresent(Order.self, forKey: .order)\n\n        self.init(\n            references: references,\n            relationType: type,\n            order: order\n        )\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Relation/RelationType.swift",
    "content": "//\n//  RelationType.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 01. 21..\n//\n\n/// Defines the cardinality of a content relation, indicating whether it links to one or multiple items.\npublic enum RelationType: String, Codable, Equatable, CaseIterable {\n    /// A one-to-one relation. The relation targets a single content item.\n    case one\n\n    /// A one-to-many relation. The relation targets a collection of content items.\n    case many\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Settings/Settings.swift",
    "content": "//\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 encoding and decoding dynamic keys.\nprivate struct DynamicCodingKeys: CodingKey {\n\n    var stringValue: String\n\n    var intValue: Int? { nil }\n\n    init?(stringValue: String) {\n        self.stringValue = stringValue\n    }\n\n    init?(intValue _: Int) {\n        nil\n    }\n}\n\n/// Represents site-wide configuration settings, allowing for dynamic, user-defined values.\npublic struct Settings: Codable, Equatable {\n\n    /// The default, empty settings instance.\n    public static var defaults: Self {\n        .init([:])\n    }\n\n    /// A dictionary holding arbitrary user-defined settings keyed by strings.\n    public var values: [String: AnyCodable]\n\n    /// Creates a new `Settings` instance with the specified key-value pairs.\n    ///\n    /// - Parameter values: A dictionary of custom settings.\n    public init(\n        _ values: [String: AnyCodable]\n    ) {\n        self.values = values\n    }\n\n    /// Initializes a `Settings` instance by decoding from the given decoder.\n    ///\n    /// If decoding fails, initializes with default empty settings.\n    ///\n    /// - Parameter decoder: The decoder to read data from.\n    /// - Throws: An error if decoding fails unexpectedly.\n    public init(\n        from decoder: any Decoder\n    ) throws {\n        guard\n            let container = try? decoder.singleValueContainer(),\n            let value = try? container.decode([String: AnyCodable].self)\n        else {\n            self.values = Self.defaults.values\n            return\n        }\n        self.values = value\n    }\n\n    /// Encodes the `Settings` instance into the given encoder.\n    ///\n    /// - Parameter encoder: The encoder to write data to.\n    /// - Throws: An error if any value fails to encode.\n    public func encode(\n        to encoder: any Encoder\n    ) throws {\n        var container = encoder.container(keyedBy: DynamicCodingKeys.self)\n        for (key, value) in values {\n            guard let codingKey = DynamicCodingKeys(stringValue: key) else {\n                continue\n            }\n            try container.encode(value, forKey: codingKey)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Target/Target.swift",
    "content": "//\n//  Target.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 15..\n//\n\n/// Represents a deployment target configuration for a Toucan project.\npublic struct Target: Codable, Equatable {\n\n    /// Keys explicitly defined for decoding known fields from the input source.\n    enum CodingKeys: CodingKey, CaseIterable {\n        case name\n        case config\n        case url\n        case input\n        case output\n        case `default`\n    }\n\n    /// Base values used when decoding fails or fields are missing.\n    private static var base: Self {\n        .init(\n            name: \"dev\",\n            config: \"\",\n            url: \"http://localhost:3000\",\n            input: \".\",\n            output: \"dist\",\n            isDefault: false\n        )\n    }\n\n    /// Standard target value\n    public static var standard: Self {\n        var target = Self.base\n        target.isDefault = true\n        return target\n    }\n\n    /// The unique name of the target.\n    public var name: String\n\n    /// The path to the configuration file.\n    public var config: String\n\n    /// The base URL of the site or project without a trailing slash (e.g., `\"https://example.com\"`).\n    public var url: String\n\n    /// The input path for the source files.\n    public var input: String\n\n    /// The output path for generated files.\n    public var output: String\n\n    /// A flag indicating if this is the default target.\n    public var isDefault: Bool\n\n    /// Creates a new target configuration.\n    /// - Parameters:\n    ///   - name: The unique name of the target.\n    ///   - config: The path to the configuration file.\n    ///   - url: The base URL for the target.\n    ///   - input: The input path for the source files.\n    ///   - output: The output path for generated files.\n    ///   - isDefault: A flag indicating if this is the default target.\n    public init(\n        name: String,\n        config: String,\n        url: String,\n        input: String,\n        output: String,\n        isDefault: Bool\n    ) {\n        self.name = name\n        self.config = config\n        self.url = url\n        self.input = input\n        self.output = output\n        self.isDefault = isDefault\n    }\n\n    /// Custom decoder with fallback values.\n    public init(\n        from decoder: any Decoder\n    ) throws {\n        try decoder.validateUnknownKeys(keyType: CodingKeys.self)\n\n        let base = Self.base\n\n        let container = try decoder.container(keyedBy: CodingKeys.self)\n\n        self.name =\n            try container.decodeIfPresent(\n                String.self,\n                forKey: .name\n            ) ?? base.name\n\n        self.config =\n            try container.decodeIfPresent(\n                String.self,\n                forKey: .config\n            ) ?? base.config\n\n        self.url =\n            try container.decodeIfPresent(\n                String.self,\n                forKey: .url\n            ) ?? base.url\n\n        self.input =\n            try container.decodeIfPresent(\n                String.self,\n                forKey: .input\n            ) ?? base.input\n\n        self.output =\n            try container.decodeIfPresent(\n                String.self,\n                forKey: .output\n            ) ?? base.output\n\n        self.isDefault =\n            try container.decodeIfPresent(\n                Bool.self,\n                forKey: .default\n            ) ?? base.isDefault\n    }\n\n    /// Encodes this instance into the given encoder.\n    ///\n    /// - Parameter encoder: The encoder to write data to.\n    /// - Throws: An error if any values are invalid for the given encoder’s format.\n    public func encode(\n        to encoder: any Encoder\n    ) throws {\n        var container = encoder.container(keyedBy: CodingKeys.self)\n\n        try container.encode(name, forKey: .name)\n        try container.encode(config, forKey: .config)\n        try container.encode(url, forKey: .url)\n        try container.encode(input, forKey: .input)\n        try container.encode(output, forKey: .output)\n        try container.encode(isDefault, forKey: .default)\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Target/TargetConfig.swift",
    "content": "//\n//  TargetConfig.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 15..\n//\n\n/// A structure that holds a list of deployment targets and resolves the default one.\npublic struct TargetConfig: Codable, Equatable {\n\n    /// Keys explicitly defined for decoding known fields from the input source.\n    enum CodingKeys: CodingKey, CaseIterable {\n        case targets\n    }\n\n    /// Default values used when decoding fails or fields are missing.\n    private static var base: Self {\n        .init(targets: [Target.standard])\n    }\n\n    /// All defined targets.\n    public var targets: [Target]\n\n    /// The default target (first one with `isDefault == true`, or first in the list, or fallback).\n    public var `default`: Target {\n        targets.first(where: { $0.isDefault }) ?? targets[0]\n    }\n\n    /// Creates a new `Targets` object.\n    /// - Parameter targets: An array of deployment targets.\n    /// - Precondition: Only one target may have `isDefault == true`.\n    public init(\n        targets: [Target]\n    ) {\n        let defaultCount = targets.filter(\\.isDefault).count\n        precondition(\n            defaultCount <= 1,\n            \"Only one target can be marked as default.\"\n        )\n\n        var all = targets\n        if !all.isEmpty, defaultCount == 0 {\n            all[0].isDefault = true\n        }\n        self.targets = all.isEmpty ? Self.base.targets : all\n    }\n\n    /// Custom decoder with fallback values and default validation.\n    ///\n    /// - Parameter decoder: The decoer used to decode values.\n    /// - Throws: An error if any values are invalid for the given encoder’s format.\n    public init(\n        from decoder: any Decoder\n    ) throws {\n        try decoder.validateUnknownKeys(keyType: CodingKeys.self)\n\n        let container = try? decoder.container(keyedBy: CodingKeys.self)\n        let all =\n            try container?\n            .decodeIfPresent(\n                [Target].self,\n                forKey: .targets\n            ) ?? []\n\n        let defaultCount = all.filter(\\.isDefault).count\n        guard defaultCount <= 1 else {\n            throw DecodingError.dataCorrupted(\n                .init(\n                    codingPath: container?.codingPath ?? [],\n                    debugDescription:\n                        \"Only one target can be marked as default.\"\n                )\n            )\n        }\n        self.init(targets: all)\n    }\n\n    /// Encodes this instance into the given encoder.\n    ///\n    /// - Parameter encoder: The encoder to write data to.\n    /// - Throws: An error if any values are invalid for the given encoder’s format.\n    public func encode(\n        to encoder: any Encoder\n    ) throws {\n        var container = encoder.container(keyedBy: CodingKeys.self)\n        try container.encode(targets, forKey: .targets)\n    }\n}\n"
  },
  {
    "path": "Sources/ToucanSource/Objects/Types/ContentType.swift",
    "content": "//\n//  ContentType.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 01. 15..\n//\n\n/// Describes a content type definition including schema, relations, and associated queries.\n///\n/// `ContentType` is used to declare how a particular content type (e.g., blog, project, product)\n/// should be parsed, validated, and queried in the pipeline.\npublic struct ContentType: Codable, Equatable {\n\n    private enum CodingKeys: CodingKey, CaseIterable {\n        case id\n        case `default`\n        case paths\n        case properties\n        case relations\n        case queries\n    }\n\n    /// A unique identifier for this content type (e.g., `\"blog\"`, `\"author\"`).\n    public var id: String\n\n    /// Indicates whether this is the default content type fallback.\n    ///\n    /// If `true`, this type will be used only when the content does not explicitly declare its type,\n    /// and no matching `paths` from other types apply.\n    ///\n    /// ⚠️ Only one content type in the system may be marked as `default`; otherwise, an error will occur.\n    public var `default`: Bool\n\n    /// A list of file path patterns (globs or prefixes) used to associate source files with this content type.\n    ///\n    /// Example: `[\"posts/**\", \"blog/*.md\"]`\n    public var paths: [String]\n\n    /// A map of property names to their type definitions.\n    ///\n    /// These represent structured, typed fields such as `title`, `published`, `authorId`, etc.\n    public var properties: [String: Property]\n\n    /// A map of relation names to their relationship configuration.\n    ///\n    /// These define links to other content (e.g., `author`, `relatedPosts`).\n    public var relations: [String: Relation]\n\n    /// Named queries that can be used within scopes or as reusable filters for rendering this type.\n    public var queries: [String: Query]\n\n    /// Creates a new instance.\n    ///\n    /// - Parameters:\n    ///   - id: Unique identifier for the content type.\n    ///   - default: Whether this is the fallback default type.\n    ///   - paths: Glob-like patterns that identify matching content files.\n    ///   - properties: Field definitions and types.\n    ///   - relations: Definitions of inter-content relationships.\n    ///   - queries: Reusable queries for list or scoped views.\n    public init(\n        id: String,\n        default: Bool = false,\n        paths: [String] = [],\n        properties: [String: Property] = [:],\n        relations: [String: Relation] = [:],\n        queries: [String: Query] = [:]\n    ) {\n        self.id = id\n        self.default = `default`\n        self.paths = paths\n        self.properties = properties\n        self.relations = relations\n        self.queries = queries\n    }\n\n    /// Decode from a structured format (e.g., YAML or JSON),\n    /// applying defaults for missing optional fields.\n    public init(\n        from decoder: any Decoder\n    ) throws {\n        try decoder.validateUnknownKeys(keyType: CodingKeys.self)\n\n        let container = try decoder.container(keyedBy: CodingKeys.self)\n\n        let id = try container.decode(\n            String.self,\n            forKey: .id\n        )\n\n        let `default` =\n            (try? container.decode(\n                Bool.self,\n                forKey: .default\n            )) ?? false\n\n        let paths =\n            try container.decodeIfPresent(\n                [String].self,\n                forKey: .paths\n            ) ?? []\n\n        var properties =\n            try container.decodeIfPresent(\n                [String: Property].self,\n                forKey: .properties\n            ) ?? [:]\n\n        // Providing system properties\n        properties[SystemPropertyKeys.id.rawValue] = .init(\n            propertyType: .string,\n            isRequired: true\n        )\n        properties[SystemPropertyKeys.lastUpdate.rawValue] = .init(\n            propertyType: .string,\n            isRequired: true\n        )\n        properties[SystemPropertyKeys.slug.rawValue] = .init(\n            propertyType: .string,\n            isRequired: true\n        )\n        properties[SystemPropertyKeys.type.rawValue] = .init(\n            propertyType: .string,\n            isRequired: true\n        )\n\n        let relations =\n            try container.decodeIfPresent(\n                [String: Relation].self,\n                forKey: .relations\n            ) ?? [:]\n\n        let queries =\n            try container.decodeIfPresent(\n                [String: Query].self,\n                forKey: .queries\n            ) ?? [:]\n\n        self.init(\n            id: id,\n            default: `default`,\n            paths: paths,\n            properties: properties,\n            relations: relations,\n            queries: queries\n        )\n    }\n}\n"
  },
  {
    "path": "Sources/_GitCommitHash/git_commit_hash.c",
    "content": "#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",
    "content": "#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",
    "content": "//\n//  Entrypoint.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 04. 15..\n\nimport ArgumentParser\nimport Dispatch\nimport Foundation\nimport SwiftCommand\nimport ToucanCore\n\nextension Array {\n    mutating func popFirst() -> Element? {\n        isEmpty ? nil : removeFirst()\n    }\n}\n\n/// The main entry point for the command-line tool.\n@main\nstruct Entrypoint: AsyncParsableCommand {\n\n    /// Configuration for the command-line tool.\n    static let configuration = CommandConfiguration(\n        commandName: \"toucan\",\n        abstract: \"\"\"\n            Toucan\n            \"\"\",\n        discussion: \"\"\"\n            A markdown-based Static Site Generator (SSG) written in Swift.\n            \"\"\",\n        version: GeneratorInfo.current.version,\n        helpNames: []\n    )\n\n    @Argument(parsing: .allUnrecognized)\n    var subcommand: [String]\n\n    func run() async throws {\n        var args = CommandLine.arguments\n\n        guard\n            args.count > 1,\n            let path = args.popFirst(),\n            let subcommand = args.popFirst()\n        else {\n            fatalError(\n                \"Missing arguments, at least one subcommand is required.\"\n            )\n        }\n\n        let base = URL(fileURLWithPath: path).lastPathComponent\n        let toucanCmd = base + \"-\" + subcommand\n\n        if subcommand.isEmpty || subcommand == \"--help\" || subcommand == \"-h\" {\n            displayHelp()\n            return\n        }\n        if subcommand == \"--version\" {\n            displayVersion()\n            return\n        }\n\n        guard let exe = Command.findInPath(withName: toucanCmd) else {\n            fatalError(\"Subcommand not found: `\\(toucanCmd)`.\")\n        }\n        let cmd =\n            exe\n            .addArguments(args)\n            .setStdin(.pipe(closeImplicitly: false))\n            .setStdout(.inherit)\n            .setStderr(.inherit)\n\n        let subprocess = try cmd.spawn()\n\n        let signalSource = DispatchSource.makeSignalSource(\n            signal: SIGINT,\n            queue: .main\n        )\n        signal(SIGINT, SIG_IGN)  // Ignore default SIGINT behavior\n\n        signalSource.setEventHandler {\n            if subprocess.isRunning {\n                subprocess.interrupt()\n            }\n        }\n        signalSource.resume()\n\n        try subprocess.wait()\n    }\n\n    private func displayVersion() {\n        print(Self.configuration.version)\n    }\n\n    private func displayHelp() {\n        print(\n            \"\"\"\n            OVERVIEW: \\(Self.configuration.abstract)\n\n            \\(Self.configuration.discussion)\n\n            USAGE:\n            toucan <subcommand>\n\n            SUBCOMMANDS:\n            init            Initializes a new Toucan project\n            generate        Build static files using configured targets\n            watch           Watch for changes and auto-regenerate output\n            serve           Start a local web server to preview the site\n\n            OPTIONS:\n            --version       Show the version.\n            -h, --help      Show help information.\n            \"\"\"\n        )\n    }\n\n}\n"
  },
  {
    "path": "Sources/toucan-generate/Entrypoint.swift",
    "content": "//\n//  Entrypoint.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 04. 15..\n\nimport ArgumentParser\nimport Logging\nimport ToucanCore\nimport ToucanSDK\n\nextension Logger.Level: @retroactive ExpressibleByArgument {}\n\n/// The main entry point for the command-line tool.\n@main\nstruct Entrypoint: AsyncParsableCommand {\n\n    /// Configuration for the command-line tool.\n    static let configuration = CommandConfiguration(\n        commandName: \"toucan-generate\",\n        abstract: \"\"\"\n            Toucan Generate Command\n            \"\"\",\n        discussion: \"\"\"\n            Generates static files for your website using the selected build target.\n            \"\"\",\n        version: GeneratorInfo.current.release.description\n    )\n\n    @Argument(\n        help: \"\"\"\n                The working directory to look for a `toucan.yml` file.  \n                \n                Default: current working directory\n            \"\"\"\n    )\n    var workDir: String = \".\"\n\n    @Option(\n        name: .shortAndLong,\n        help: \"The target to build, if empty build all.\"\n    )\n    var target: String?\n\n    func run() async throws {\n        let logger = Logger.subsystem(\"generate\")\n\n        var targetsToBuild: [String] = []\n        if let target, !target.isEmpty {\n            targetsToBuild.append(target)\n        }\n\n        let generator = Toucan()\n\n        if generator.generateAndLogErrors(\n            workDir: workDir,\n            targetsToBuild: targetsToBuild,\n            now: .init()\n        ) {\n            let metadata: Logger.Metadata = [\n                \"workDir\": .string(workDir)\n            ]\n            logger.info(\"Site generated successfully.\", metadata: metadata)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/toucan-init/Download.swift",
    "content": "//\n//  Download.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 03. 31..\n\nimport FileManagerKit\nimport Foundation\nimport SwiftCommand\n\nstruct Download {\n    let id = UUID().uuidString\n    let sourceURL: URL\n    let targetDirURL: URL\n    let fileManager: FileManager\n\n    private var url: URL {\n        fileManager.temporaryDirectory.appendingPathComponent(id)\n    }\n\n    private var zipURL: URL {\n        url.appendingPathExtension(\"zip\")\n    }\n\n    func resolve() async throws {\n        /// Ensure working directory exists\n        try fileManager.createDirectory(\n            at: url,\n            withIntermediateDirectories: true\n        )\n        let zipURL = url.appendingPathExtension(\"zip\")\n\n        /// check if the target directory exists\n        if !fileManager.fileExists(at: targetDirURL) {\n            try fileManager.createDirectory(\n                at: targetDirURL,\n                withIntermediateDirectories: true\n            )\n        }\n\n        /// Find and run `curl` using SwiftCommand\n        guard let curl = Command.findInPath(withName: \"curl\") else {\n            fatalError(\"Command not found: 'curl'\")\n        }\n        _ =\n            try await curl\n            .addArguments([\n                \"-L\",\n                sourceURL.absoluteString,\n                \"-o\",\n                zipURL.path,\n            ])\n            .output\n\n        /// Find and run `unzip` using SwiftCommand\n        guard let unzipExe = Command.findInPath(withName: \"unzip\") else {\n            fatalError(\"Command not found 'unzip'\")\n        }\n        _ =\n            try await unzipExe\n            .addArguments([zipURL.path, \"-d\", url.path])\n            .output\n\n        /// Remove existing target directory\n        try? fileManager.removeItem(at: targetDirURL)\n\n        /// Finding the root directory URL.\n        let items = fileManager.listDirectory(at: url)\n        guard let rootDirName = items.first else {\n            throw URLError(.cannotParseResponse)\n        }\n        let rootDirURL = url.appendingPathComponent(rootDirName)\n\n        /// Moving files to the target directory.\n        try fileManager.moveItem(at: rootDirURL, to: targetDirURL)\n\n        /// Cleaning up unnecessary files.\n        try? fileManager.delete(at: zipURL)\n        try? fileManager.delete(at: url)\n    }\n}\n"
  },
  {
    "path": "Sources/toucan-init/Entrypoint.swift",
    "content": "//\n//  Entrypoint.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 04. 15..\n\nimport ArgumentParser\nimport FileManagerKit\nimport Foundation\nimport Logging\nimport ToucanCore\nimport ToucanSource\n\nextension Logger.Level: @retroactive ExpressibleByArgument {}\n\n/// The main entry point for the command-line tool.\n@main\nstruct Entrypoint: AsyncParsableCommand {\n    /// Configuration for the command-line tool.\n    static let configuration = CommandConfiguration(\n        commandName: \"toucan-init\",\n        abstract: \"\"\"\n            Toucan Init Command\n            \"\"\",\n        discussion: \"\"\"\n            Initializes a new Toucan project. Creates required folders and files in the specified directory.\n            \"\"\",\n        version: GeneratorInfo.current.release.description\n    )\n\n    @Argument(help: \"The name of the site directory (default: site).\")\n    var siteDirectory: String = \"site\"\n\n    @Option(\n        name: .shortAndLong,\n        help:\n            \"Specifies a URL to a remote zip file containing a demo project to use as the starting point. If not specified, a minimal setup will be used.\"\n    )\n    var demoSourceZipURL: String?\n\n    func run() async throws {\n        let logger = Logger.subsystem(\"init\")\n\n        let siteExists = fileManager.directoryExists(at: siteDirectoryURL)\n\n        guard !siteExists else {\n            logger.error(\"Folder already exists: \\(siteDirectoryURL)\")\n            return\n        }\n\n        do {\n            let sourceUrl = demoSourceZipURL.flatMap { URL(string: $0) }\n\n            let source = Download(\n                sourceURL: sourceUrl ?? minimalSourceURL,\n                targetDirURL: siteDirectoryURL,\n                fileManager: fileManager\n            )\n\n            logger.trace(\"Preparing files.\")\n            try await source.resolve()\n\n            logger.trace(\"'\\(siteDirectory)' was prepared successfully.\")\n        }\n        catch {\n            logger.error(\"\\(String(describing: error))\")\n        }\n    }\n}\n\nextension Entrypoint {\n    var fileManager: FileManager { .default }\n\n    var currentDirectoryURL: URL {\n        URL(fileURLWithPath: fileManager.currentDirectoryPath)\n    }\n\n    var siteDirectoryURL: URL {\n        currentDirectoryURL.appendingPathComponent(siteDirectory)\n    }\n\n    var minimalSourceURL: URL {\n        .init(\n            string:\n                \"https://github.com/toucansites/minimal-template-demo/archive/refs/heads/main.zip\"\n        )!\n    }\n}\n"
  },
  {
    "path": "Sources/toucan-serve/Entrypoint.swift",
    "content": "//\n//  Entrypoint.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 04. 15..\n\nimport ArgumentParser\nimport Foundation\nimport Hummingbird\nimport Logging\nimport ToucanCore\n\nextension Logger.Level: @retroactive ExpressibleByArgument {}\n\n/// The main entry point for the command-line tool.\n@main\nstruct Entrypoint: AsyncParsableCommand {\n    /// Configuration for the command-line tool.\n    static let configuration = CommandConfiguration(\n        commandName: \"toucan-serve\",\n        abstract: \"\"\"\n            Toucan Serve Command\n            \"\"\",\n        discussion: \"\"\"\n            Starts a local web server to serve a specified directory.\n            \"\"\",\n        version: GeneratorInfo.current.release.description\n    )\n\n    @Argument(help: \"The root directory (default: dist).\")\n    var root: String = \"./dist\"\n\n    @Option(name: .shortAndLong)\n    var address: String = \"0.0.0.0\"\n\n    @Option(name: .shortAndLong)\n    var port: Int = 3000\n\n    func run() async throws {\n        let home = FileManager.default.homeDirectoryForCurrentUser.path\n        var rootPath = root.replacing(\"~\", with: home)\n        if rootPath.hasPrefix(\".\") {\n            rootPath =\n                FileManager.default.currentDirectoryPath + \"/\" + rootPath\n        }\n\n        let router = Router()\n        let logger = Logger.subsystem(\"serve\")\n\n        router.addMiddleware {\n            NotFoundMiddleware()\n            FileMiddleware(\n                rootPath,\n                searchForIndexHtml: true,\n                logger: logger\n            )\n        }\n\n        let app = Application(\n            router: router,\n            configuration: .init(\n                address: .hostname(address, port: port),\n                serverName: \"toucan-server\"\n            ),\n            logger: logger\n        )\n        try await app.runService()\n    }\n}\n"
  },
  {
    "path": "Sources/toucan-serve/NotFoundMiddleware.swift",
    "content": "//\n//  NotFoundMiddleware.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 01. 23..\n//\n\nimport Hummingbird\n\nstruct NotFoundMiddleware<Context: RequestContext>: RouterMiddleware {\n    func handle(\n        _ request: Request,\n        context: Context,\n        next: (\n            Request,\n            Context\n        ) async throws -> Response\n    ) async throws -> Response {\n        do {\n            return try await next(request, context)\n        }\n        catch let error as HTTPError {\n            if error.status == .notFound {\n                return Response(\n                    status: .seeOther,\n                    headers: [\n                        .location: \"/404.html\"\n                    ]\n                )\n            }\n            throw error\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/toucan-watch/Entrypoint.swift",
    "content": "//\n//  Entrypoint.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 04. 15..\n\nimport ArgumentParser\nimport FileMonitor\nimport Foundation\nimport Logging\nimport SwiftCommand\nimport ToucanCore\n\nextension Logger.Level: @retroactive ExpressibleByArgument {}\n\n/// The main entry point for the command-line tool.\n@main\nstruct Entrypoint: AsyncParsableCommand {\n    /// Configuration for the command-line tool.\n    static let configuration = CommandConfiguration(\n        commandName: \"toucan-watch\",\n        abstract: \"\"\"\n            Toucan Watch Command\n            \"\"\",\n        discussion: \"\"\"\n            Watches your project directory for changes and automatically rebuilds output files when content is updated.\n            \"\"\",\n        version: GeneratorInfo.current.release.description\n    )\n\n    @Argument(help: \"The input directory (default: current working directory).\")\n    var input: String = \".\"\n\n    @Option(\n        name: .shortAndLong,\n        help:\n            \"The directory to ignore, relative to the working directory (default: dist).\"\n    )\n    var ignore: String = \"dist\"\n\n    @Option(\n        name: .shortAndLong,\n        help: \"The target to build, if empty build all.\"\n    )\n    var target: String?\n\n    @Option(\n        name: .shortAndLong,\n        help: \"The treshold to watch for changes in seconds (default: 3).\"\n    )\n    var seconds: Int = 3\n\n    var arguments: [String] {\n        [input] + options\n    }\n\n    var options: [String] {\n        var options: [String] = []\n        if let target, !target.isEmpty {\n            options.append(\"--target\")\n            options.append(target)\n        }\n        return options\n    }\n\n    func run() async throws {\n        let logger = Logger.subsystem(\"watch\")\n\n        let metadata: Logger.Metadata = [\n            \"input\": .string(input),\n            \"target\": .string(target ?? \"(default)\"),\n            \"ignore\": .string(ignore),\n            \"treshold\": .string(String(seconds)),\n        ]\n\n        logger.info(\n            \"👀 Watching Toucan site.\",\n            metadata: metadata\n        )\n\n        //\n        // NOTE: To test this feature\n        //\n        // 1. Make sure Toucan is installed somwehere.\n        // 2. Edit scheme in Xcode or use `setenv`, e.g.:\n        //     `setenv(\"PATH\", \"/usr/local/bin\", 1)`\n        // 3. Set a `PATH` environment variable:\n        //      `PATH=/usr/local/bin`\n        //\n        let currentToucanCommand = Command.findInPath(withName: \"toucan\")\n        let toucanCommandUrl = currentToucanCommand?.executablePath.string\n\n        guard let toucan = toucanCommandUrl,\n            FileManager.default.isExecutableFile(atPath: toucan)\n        else {\n            logger.error(\n                \"Toucan is not installed.\",\n                metadata: metadata\n            )\n            return\n        }\n\n        let inputURL = safeURL(for: input)\n\n        let commandURL = URL(fileURLWithPath: toucan)\n        let command = Command(\n            executablePath: .init(commandURL.path() + \"-generate\")\n        )\n        .addArguments(arguments)\n\n        let generate = try await command.output.stdout\n\n        if !generate.isEmpty {\n            logger.debug(\n                .init(stringLiteral: generate),\n                metadata: metadata\n            )\n            return\n        }\n\n        let ignoreURL = inputURL.appendingPathIfPresent(ignore)\n\n        let monitor = try FileMonitor(directory: inputURL)\n        try monitor.start()\n        for await event in monitor.stream.debounce(for: .seconds(seconds)) {\n\n            let eventPath = event.url.path()\n            guard !eventPath.hasPrefix(ignoreURL.path()) else {\n                logger.trace(\n                    \"Skipping generation due to ignore path.\",\n                    metadata: metadata\n                )\n                continue\n            }\n\n            logger.info(\n                \"Generating site.\",\n                metadata: metadata\n            )\n\n            let generate = try await command.output.stdout\n\n            if !generate.isEmpty {\n                logger.debug(\n                    .init(stringLiteral: generate),\n                    metadata: metadata\n                )\n                return\n            }\n        }\n    }\n\n    func safeURL(for path: String) -> URL {\n        let home = FileManager.default.homeDirectoryForCurrentUser.path\n        let replaced = path.replacing(\"~\", with: home)\n        return .init(fileURLWithPath: replaced).standardized\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanCoreTests/Extensions/StringExtensionsTestSuite.swift",
    "content": "//\n//  StringExtensionsTestSuite.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 17..\n//\n\nimport Testing\n\n@testable import ToucanCore\n\n@Suite\nstruct StringExtensionsTestSuite {\n\n    // MARK: - URL (slug) validation\n\n    @Test\n    func specifiedCharactersPass() {\n        let alphabet = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\"\n        let numerics = \"0123456789\"\n        let special = \"-._~{}%\"\n        let reserved = \":/?#[]@!$&'()*+,;=\"\n\n        #expect(alphabet.containsOnlyValidURLCharacters())\n        #expect(numerics.containsOnlyValidURLCharacters())\n        #expect(special.containsOnlyValidURLCharacters())\n        #expect(reserved.containsOnlyValidURLCharacters())\n    }\n\n    @Test\n    func percentEncodingAllowed() {\n        #expect(\"%\".containsOnlyValidURLCharacters())\n        #expect(\"hello%20world\".containsOnlyValidURLCharacters())\n    }\n\n    @Test\n    func mixedValidStringPasses() {\n        #expect(\n            \"https://example.com/a-b_c~d.e?x=1&y=2#frag\"\n                .containsOnlyValidURLCharacters()\n        )\n    }\n\n    @Test\n    func spaceFails() {\n        #expect(!\"hello world\".containsOnlyValidURLCharacters())\n        #expect(!\" \".containsOnlyValidURLCharacters())\n    }\n\n    @Test\n    func nonASCIIFails() {\n        #expect(!\"café\".containsOnlyValidURLCharacters())\n        #expect(!\"東京\".containsOnlyValidURLCharacters())\n        #expect(!\"🙂\".containsOnlyValidURLCharacters())\n    }\n\n    @Test\n    func punctuationOutsideSpecFails() {\n        #expect(!\"\\\"\".containsOnlyValidURLCharacters())\n        #expect(!\"<\".containsOnlyValidURLCharacters())\n        #expect(!\"\\\\\".containsOnlyValidURLCharacters())\n        #expect(!\"{|}\".containsOnlyValidURLCharacters())\n    }\n\n    @Test\n    func emptyURLStringIsValid() {\n        #expect(\"\".containsOnlyValidURLCharacters())\n    }\n\n    @Test\n    func percentTripletStructureNotEnforced() {\n        #expect(\"%2G\".containsOnlyValidURLCharacters())\n        #expect(\"%ZZ\".containsOnlyValidURLCharacters())\n    }\n\n    // MARK: - path validation\n\n    @Test\n    func validPathCharactersPass() {\n        #expect(\"documents/swift-guide.txt\".containsOnlyValidPathCharacters())\n        #expect(\"images/profile_photo-01.png\".containsOnlyValidPathCharacters())\n        #expect(\"{{page.iterator}}\".containsOnlyValidPathCharacters())\n        #expect(\"[01](fo_o)-bar:special\".containsOnlyValidPathCharacters())\n    }\n\n    @Test\n    func disallowedPercentFails() {\n        #expect(!\"hello%20world\".containsOnlyValidPathCharacters())\n    }\n\n    @Test\n    func disallowedQuestionMarkFails() {\n        #expect(!\"file?name.txt\".containsOnlyValidPathCharacters())\n    }\n\n    @Test\n    func disallowedHashFails() {\n        #expect(!\"docs#section\".containsOnlyValidPathCharacters())\n    }\n\n    @Test\n    func disallowedAmpersandFails() {\n        #expect(!\"a&b.txt\".containsOnlyValidPathCharacters())\n    }\n\n    @Test\n    func disallowedEqualsFails() {\n        #expect(!\"key=value\".containsOnlyValidPathCharacters())\n    }\n\n    @Test\n    func emptyPathStringIsValid() {\n        #expect(\"\".containsOnlyValidPathCharacters())\n    }\n\n    @Test\n    func mixedValidCharactersPass() {\n        #expect(\n            \"folder/sub-folder/file_name-123.txt\"\n                .containsOnlyValidPathCharacters()\n        )\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanCoreTests/Extensions/URLExtensionsTestSuite.swift",
    "content": "//\n//  URLExtensionsTestSuite.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 17..\n//\n\nimport Foundation\nimport Testing\n\n@testable import ToucanCore\n\n@Suite\nstruct URLExtensionsTestSuite {\n    @Test\n    func appendingValidPath() {\n        let base = URL(string: \"https://example.com\")!\n        let result = base.appendingPathIfPresent(\"users\")\n        #expect(result.absoluteString == \"https://example.com/users\")\n    }\n\n    @Test\n    func appendingEmptyPath() {\n        let base = URL(string: \"https://example.com\")!\n        let result = base.appendingPathIfPresent(\"\")\n        #expect(result.absoluteString == \"https://example.com\")\n    }\n\n    @Test\n    func appendingNilPath() {\n        let base = URL(string: \"https://example.com\")!\n        let result = base.appendingPathIfPresent(nil)\n        #expect(result.absoluteString == \"https://example.com\")\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanCoreTests/ToucanCoreTestSuite.swift",
    "content": "//\n//  ToucanCoreTestSuite.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 17..\n//\n\nimport Testing\n@testable import ToucanCore\n\n@Suite\nstruct ToucanCoreTestSuite {\n\n    @Test()\n    func currentRelease() async throws {\n\n        // Make sure to update the target release\n        let targetRelease = GeneratorInfo.v1_0_0.release\n\n        let currentRelease = GeneratorInfo.current.release\n        #expect(targetRelease == currentRelease)\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanMarkdownTests/ContentRendererTestSuite.swift",
    "content": "//\n//  ContentRendererTestSuite.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 04. 15..\n\nimport Foundation\nimport Logging\nimport Testing\n\n@testable import ToucanMarkdown\n\n@Suite\nstruct MarkdownRendererTestSuite {\n    @Test\n    func basicRendering() throws {\n        let logger = Logger(label: \"ContentRendererTestSuite\")\n        let renderer = MarkdownRenderer(\n            configuration: .init(\n                markdown: .init(\n                    customBlockDirectives: [\n                        MarkdownBlockDirective.Mocks.faq()\n                    ]\n                ),\n                outline: .init(\n                    levels: [2, 3]\n                ),\n                readingTime: .init(\n                    wordsPerMinute: 238\n                ),\n                transformerPipeline: nil,\n                paragraphStyles: [:]\n            ),\n            logger: logger\n        )\n\n        let input = #\"\"\"\n            @FAQ {\n                ## test \n                Lorem ipsum\n            }\n            \"\"\"#\n\n        let contents = renderer.render(\n            content: input,\n            typeAwareID: \"\",\n            slug: \"\",\n            assetsPath: \"\",\n            baseURL: \"\"\n        )\n\n        let html = #\"\"\"\n            <div class=\"faq\"><h2 id=\"test\">test</h2><p>Lorem ipsum</p></div>\n            \"\"\"#\n\n        #expect(contents.html == html)\n        #expect(\n            contents.outline == [\n                .init(\n                    level: 2,\n                    text: \"test\",\n                    fragment: \"test\"\n                )\n            ]\n        )\n        #expect(contents.readingTime == 1)\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanMarkdownTests/HTMLVisitorTestSuite.swift",
    "content": "//\n//  HTMLVisitorTestSuite.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 04. 15..\n\nimport Logging\nimport Markdown\nimport Testing\n\n@testable import ToucanMarkdown\n\n@Suite\nstruct HTMLVisitorTestSuite {\n    func renderHTML(\n        markdown: String,\n        baseURL: String? = nil\n    ) -> String {\n        let document = Document(\n            parsing: markdown,\n            options: []\n        )\n\n        var visitor = HTMLVisitor(\n            blockDirectives: [],\n            paragraphStyles: [\n                \"note\": [\"note\"],\n                \"warning\": [\"warn\", \"warning\"],\n                \"tip\": [\"tip\"],\n                \"important\": [\"important\"],\n                \"error\": [\"error\", \"caution\"],\n            ],\n            slug: \"slug\",\n            assetsPath: \"assets\",\n            baseURL: baseURL ?? \"http://localhost:3000\"\n        )\n\n        return visitor.visit(document)\n    }\n\n    // MARK: - standard elements\n\n    @Test\n    func rawHTML() {\n        let input = #\"\"\"\n            <p><b>https://swift.org</b></p>\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n            .trimmingCharacters(in: .whitespacesAndNewlines)\n\n        let expectation = #\"\"\"\n            <p><b>https://swift.org</b></p>\n            \"\"\"#\n            .trimmingCharacters(in: .whitespacesAndNewlines)\n\n        //        // with escaping\n        //        let expectation = #\"\"\"\n        //            &lt;p&gt;&lt;b&gt;https://swift.org&lt;/b&gt;&lt;/p&gt;\n        //            \"\"\"#\n        //            .trimmingCharacters(in: .whitespacesAndNewlines)\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func inlineHTML() {\n        let input = #\"\"\"\n            lorem <b>https://swift.org</b> ipsum\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n\n        let expectation = #\"\"\"\n            <p>lorem &lt;b&gt;https://swift.org&lt;/b&gt; ipsum</p>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func paragraph() {\n        let input = #\"\"\"\n            Lorem ipsum dolor sit amet.\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n\n        let expectation = #\"\"\"\n            <p>Lorem ipsum dolor sit amet.</p>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func softBreak() {\n        let input = #\"\"\"\n            This is the first line.\n            And this is the second line.\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n\n        let expectation = #\"\"\"\n            <p>This is the first line.<br>And this is the second line.</p>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func lineBreak() {\n        let input = #\"\"\"\n            a\\\n            b\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n\n        let expectation = #\"\"\"\n            <p>a<br>b</p>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func thematicBreak() {\n        let input = #\"\"\"\n            Lorem ipsum\n            ***\n            dolor\n            ---\n            sit\n            _________________\n            amet.\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n\n        let expectation = #\"\"\"\n            <p>Lorem ipsum</p><hr><h2 id=\"dolor\">dolor</h2><p>sit</p><hr><p>amet.</p>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func strong() {\n        let input = #\"\"\"\n            Lorem **ipsum** dolor __sit__ amet.\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n\n        let expectation = #\"\"\"\n            <p>Lorem <strong>ipsum</strong> dolor <strong>sit</strong> amet.</p>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func striketrough() {\n        let input = #\"\"\"\n            Lorem ipsum ~~dolor sit amet~~.\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n\n        let expectation = #\"\"\"\n            <p>Lorem ipsum <s>dolor sit amet</s>.</p>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func blockquote() {\n        let input = #\"\"\"\n            > Lorem ipsum dolor sit amet.\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n\n        let expectation = #\"\"\"\n            <blockquote><p>Lorem ipsum dolor sit amet.</p></blockquote>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func blockquoteNote() {\n        let input = #\"\"\"\n            > NOTE: Lorem ipsum dolor sit amet.\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n\n        let expectation = #\"\"\"\n            <blockquote class=\"note\"><p>Lorem ipsum dolor sit amet.</p></blockquote>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func blockquoteWarn() {\n        let input = #\"\"\"\n            > WARN: Lorem ipsum dolor sit amet.\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <blockquote class=\"warning\"><p>Lorem ipsum dolor sit amet.</p></blockquote>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func blockquoteWarning() {\n        let input = #\"\"\"\n            > warning: Lorem ipsum dolor sit amet.\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <blockquote class=\"warning\"><p>Lorem ipsum dolor sit amet.</p></blockquote>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func nestedBlockquote() {\n        let input = #\"\"\"\n            > Lorem ipsum\n            >\n            >> dolor __sit__ amet.\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <blockquote><p>Lorem ipsum</p><blockquote><p>dolor <strong>sit</strong> amet.</p></blockquote></blockquote>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func emphasis() {\n        let input = #\"\"\"\n            Lorem *ipsum* dolor _sit_ amet.\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <p>Lorem <em>ipsum</em> dolor <em>sit</em> amet.</p>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    // MARK: - headings\n\n    @Test\n    func h1() {\n        let input = #\"\"\"\n            # Lorem ipsum dolor sit amet.\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <h1>Lorem ipsum dolor sit amet.</h1>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func h2() {\n        let input = #\"\"\"\n            ## Lorem ipsum dolor sit amet.\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <h2 id=\"lorem-ipsum-dolor-sit-amet.\">Lorem ipsum dolor sit amet.</h2>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func h3() {\n        let input = #\"\"\"\n            ### Lorem ipsum dolor sit amet.\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <h3 id=\"lorem-ipsum-dolor-sit-amet.\">Lorem ipsum dolor sit amet.</h3>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func h4() {\n        let input = #\"\"\"\n            #### Lorem ipsum dolor sit amet.\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <h4>Lorem ipsum dolor sit amet.</h4>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func h5() {\n        let input = #\"\"\"\n            ##### Lorem ipsum dolor sit amet.\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <h5>Lorem ipsum dolor sit amet.</h5>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func h6() {\n        let input = #\"\"\"\n            ###### Lorem ipsum dolor sit amet.\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <h6>Lorem ipsum dolor sit amet.</h6>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func invalidHeading() {\n        /// NOTE: this should be treated as a paragraph\n        let input = #\"\"\"\n            ####### Lorem ipsum dolor sit amet.\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <p>####### Lorem ipsum dolor sit amet.</p>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func headingWithCode() {\n        let input = #\"\"\"\n            # Lorem <b>ipsum</b> **dolor** `sit` amet.\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <h1>Lorem &lt;b&gt;ipsum&lt;/b&gt; <strong>dolor</strong> <code>sit</code> amet.</h1>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    // MARK: - lists\n\n    @Test\n    func unorderedList() {\n        let input = #\"\"\"\n            - foo\n            - bar\n            - baz\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <ul><li>foo</li><li>bar</li><li>baz</li></ul>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func orderedList() {\n        let input = #\"\"\"\n            1. foo\n            2. bar\n            3. baz\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <ol><li>foo</li><li>bar</li><li>baz</li></ol>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func orderedListWithStartIndex() {\n        let input = #\"\"\"\n            2. foo\n            3. bar\n            4. baz\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <ol start=\"2\"><li>foo</li><li>bar</li><li>baz</li></ol>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func listWithCode() {\n        let input = #\"\"\"\n            - foo `aaa`\n            - bar\n            - baz\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <ul><li>foo <code>aaa</code></li><li>bar</li><li>baz</li></ul>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    // MARK: - other elements\n\n    @Test\n    func inlineCode() {\n        let input = #\"\"\"\n            Lorem `ipsum dolor` sit amet.\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <p>Lorem <code>ipsum dolor</code> sit amet.</p>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func link() {\n        let input = #\"\"\"\n            [Swift](https://swift.org/)\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <p><a href=\"https://swift.org/\" target=\"_blank\">Swift</a></p>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func emptyLink() {\n        let input = #\"\"\"\n            [Swift]()\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <p><a>Swift</a></p>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func dotLink() {\n        let input = #\"\"\"\n            [Swift](./foo)\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <p><a href=\"./foo\">Swift</a></p>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func slashLink() {\n        let input = #\"\"\"\n            [Swift](/foo)\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <p><a href=\"http://localhost:3000/foo\">Swift</a></p>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func externalLink() {\n        let input = #\"\"\"\n            [Swift](foo/bar)\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <p><a href=\"foo/bar\" target=\"_blank\">Swift</a></p>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func anchorLink() {\n        let input = #\"\"\"\n            [Swift](#anchor)\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <p><a href=\"#anchor\">Swift</a></p>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func anchorName() {\n        let input = #\"\"\"\n            [Swift](#[name]anchor)\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <p><a name=\"anchor\">Swift</a></p>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func image() {\n        let input = #\"\"\"\n            ![Lorem](lorem.jpg)\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <p><img src=\"lorem.jpg\" alt=\"Lorem\"></p>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func imageAssetsPrefix() {\n        let input = #\"\"\"\n            ![Lorem](./assets/lorem.jpg)\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <p><img src=\"http://localhost:3000/assets/slug/lorem.jpg\" alt=\"Lorem\"></p>\n            \"\"\"#\n        #expect(output == expectation)\n    }\n\n    @Test\n    func imageEmptySource() {\n        let input = #\"\"\"\n            ![Lorem]()\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <p></p>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func imageWithTitle() {\n        let input = #\"\"\"\n            ![Lorem](lorem.jpg \"Image title\")\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <p><img src=\"lorem.jpg\" alt=\"Lorem\" title=\"Image title\"></p>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func imageWithEmptyBaseURL() {\n        let input = #\"\"\"\n            ![Lorem](/lorem.jpg \"Image title\")\n            \"\"\"#\n        let output = renderHTML(markdown: input, baseURL: \"\")\n        let expectation = #\"\"\"\n            <p><img src=\"/lorem.jpg\" alt=\"Lorem\" title=\"Image title\"></p>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func imageWithBaseURLMarkdownValue() {\n        let input = #\"\"\"\n            ![Lorem](/lorem.jpg)\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <p><img src=\"http://localhost:3000/lorem.jpg\" alt=\"Lorem\"></p>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func imageWithBaseURLMarkdownValueNoTraling() {\n        let input = #\"\"\"\n            ![Lorem](/lorem.jpg)\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <p><img src=\"http://localhost:3000/lorem.jpg\" alt=\"Lorem\"></p>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func codeBlock() {\n        let input = #\"\"\"\n            ```js\n            Lorem\n            ipsum\n            dolor\n            sit\n            amet\n            ```\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <pre><code class=\"language-js\">Lorem\n            ipsum\n            dolor\n            sit\n            amet\n            </code></pre>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func codeBlockWithHighlight() {\n        let input = #\"\"\"\n            ```css\n            Lorem\n            /*!*/\n                ipsum\n            /*.*/\n            dolor\n            sit\n            amet\n            ```\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <pre><code class=\"language-css\">Lorem\n            <span class=\"highlight\">\n                ipsum\n            </span>\n            dolor\n            sit\n            amet\n            </code></pre>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func codeBlockWithHighlightSwift() {\n        let input = #\"\"\"\n            ```swift\n            /*!*/func main() -> String/*.*/ {\n                dump(\"Hello world\")\n                return /*!*/\"foo\"/*.*/\n            }\n            ```\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <pre><code class=\"language-swift\"><span class=\"highlight\">func main() -&gt; String</span> {\n                dump(\"Hello world\")\n                return <span class=\"highlight\">\"foo\"</span>\n            }\n            </code></pre>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func table() {\n        let input = #\"\"\"\n            | Item              | In Stock | Price |\n            | :---------------- | :------: | ----: |\n            | Python Hat        |   True   | 23.99 |\n            | SQL Hat           |   True   | 23.99 |\n            | Codecademy Tee    |  False   | 19.99 |\n            | Codecademy Hoodie |  False   | 42.99 |\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <table><thead><td>Item</td><td>In Stock</td><td>Price</td></thead><tbody><tr><td>Python Hat</td><td>True</td><td>23.99</td></tr><tr><td>SQL Hat</td><td>True</td><td>23.99</td></tr><tr><td>Codecademy Tee</td><td>False</td><td>19.99</td></tr><tr><td>Codecademy Hoodie</td><td>False</td><td>42.99</td></tr></tbody></table>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func headingWithAngleBracket() {\n        let input = #\"\"\"\n            ## This <is a> bracket\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <h2 id=\"this-is-a-bracket\">This &lt;is a&gt; bracket</h2>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func codeWithAngleBracket() {\n        let input = #\"\"\"\n            See the `<head>` tag.\n            \"\"\"#\n        let output = renderHTML(markdown: input)\n        let expectation = #\"\"\"\n            <p>See the <code>&lt;head&gt;</code> tag.</p>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanMarkdownTests/MarkdownBlockDirective+Mock.swift",
    "content": "//\n//  MarkdownBlockDirective+Mock.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 04. 17..\n\nimport Foundation\nimport ToucanMarkdown\n\npublic extension MarkdownBlockDirective {\n    enum Mocks {}\n}\n\npublic extension MarkdownBlockDirective.Mocks {\n    static func highlightedTexts(\n        max: Int = 10\n    ) -> [MarkdownBlockDirective] {\n        (1...max)\n            .map { i in\n                .init(\n                    name: \"HighlightedText-\\(i)\",\n                    parameters: nil,\n                    requiresParentDirective: nil,\n                    removesChildParagraph: nil,\n                    tag: \"div\",\n                    attributes: [\n                        .init(\n                            name: \"class\",\n                            value: \"highlighted-text\"\n                        )\n                    ],\n                    output: nil\n                )\n            }\n    }\n\n    static func faq() -> MarkdownBlockDirective {\n        .init(\n            name: \"FAQ\",\n            parameters: nil,\n            requiresParentDirective: nil,\n            removesChildParagraph: nil,\n            tag: \"div\",\n            attributes: [\n                .init(name: \"class\", value: \"faq\")\n            ],\n            output: nil\n        )\n    }\n\n    static func badDirective() -> MarkdownBlockDirective {\n        .init(\n            name: \"BAD\",\n            parameters: [\n                .init(\n                    label: \"label\",\n                    isRequired: true\n                )\n            ],\n            requiresParentDirective: \"true\",\n            removesChildParagraph: nil,\n            tag: \"div\",\n            attributes: [\n                .init(name: \"att\", value: \"none\")\n            ],\n            output: nil\n        )\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanMarkdownTests/MarkdownBlockDirectiveTestSuite.swift",
    "content": "//\n//  MarkdownBlockDirectiveTestSuite.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 04. 17..\n\nimport Logging\nimport Testing\n\n@testable import ToucanMarkdown\n\n@Suite\nstruct MarkdownBlockDirectiveTestSuite {\n    @Test\n    func simpleCustomBlockDirective() throws {\n        let renderer = MarkdownToHTMLRenderer(\n            customBlockDirectives: [\n                MarkdownBlockDirective.Mocks.faq()\n            ],\n            paragraphStyles: [:]\n        )\n\n        let input = #\"\"\"\n            @FAQ {\n                Lorem ipsum\n            }\n            \"\"\"#\n\n        let output = renderer.renderHTML(\n            markdown: input,\n            slug: \"\",\n            assetsPath: \"\",\n            baseURL: \"\"\n        )\n\n        let expectation = #\"\"\"\n            <div class=\"faq\"><p>Lorem ipsum</p></div>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func simpleCustomBlockDirectiveUsingOutput() throws {\n        let renderer = MarkdownToHTMLRenderer(\n            customBlockDirectives: [\n                .init(\n                    name: \"FAQ\",\n                    parameters: nil,\n                    requiresParentDirective: nil,\n                    removesChildParagraph: nil,\n                    tag: nil,\n                    attributes: nil,\n                    output: #\"<div class=\"faq\">{{contents}}</div>\"#\n                )\n            ],\n            paragraphStyles: [:]\n        )\n\n        let input = #\"\"\"\n            @FAQ {\n                Lorem ipsum\n            }\n            \"\"\"#\n\n        let output = renderer.renderHTML(\n            markdown: input,\n            slug: \"\",\n            assetsPath: \"\",\n            baseURL: \"\"\n        )\n\n        let expectation = #\"\"\"\n            <div class=\"faq\"><p>Lorem ipsum</p></div>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func customBlockDirectiveParameters() throws {\n        let renderer = MarkdownToHTMLRenderer(\n            customBlockDirectives: [\n                .init(\n                    name: \"Grid\",\n                    parameters: [\n                        .init(\n                            label: \"columns\",\n                            isRequired: true,\n                            defaultValue: nil\n                        )\n                    ],\n                    requiresParentDirective: nil,\n                    removesChildParagraph: nil,\n                    tag: \"div\",\n                    attributes: [\n                        .init(name: \"columns\", value: \"grid-{{columns}}\")\n                    ],\n                    output: nil\n                )\n            ],\n            paragraphStyles: [:]\n        )\n\n        let input = #\"\"\"\n            @Grid(columns: 3) {\n                Lorem ipsum\n            }\n            \"\"\"#\n\n        let output = renderer.renderHTML(\n            markdown: input,\n            slug: \"\",\n            assetsPath: \"\",\n            baseURL: \"\"\n        )\n\n        let expectation = #\"\"\"\n            <div columns=\"grid-3\"><p>Lorem ipsum</p></div>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func customBlockDirectiveParametersUsingOutput() throws {\n        let renderer = MarkdownToHTMLRenderer(\n            customBlockDirectives: [\n                .init(\n                    name: \"Grid\",\n                    parameters: [\n                        .init(\n                            label: \"columns\",\n                            isRequired: true,\n                            defaultValue: nil\n                        )\n                    ],\n                    requiresParentDirective: nil,\n                    removesChildParagraph: nil,\n                    tag: nil,\n                    attributes: nil,\n                    output:\n                        #\"<div columns=\"grid-{{columns}}\">{{contents}}</div>\"#\n                )\n            ],\n            paragraphStyles: [:]\n        )\n\n        let input = #\"\"\"\n            @Grid(columns: 3) {\n                Lorem ipsum\n            }\n            \"\"\"#\n\n        let output = renderer.renderHTML(\n            markdown: input,\n            slug: \"\",\n            assetsPath: \"\",\n            baseURL: \"\"\n        )\n\n        let expectation = #\"\"\"\n            <div columns=\"grid-3\"><p>Lorem ipsum</p></div>\n            \"\"\"#\n\n        #expect(output == expectation)\n    }\n\n    @Test\n    func unrecognizedDirective() throws {\n        let renderer = MarkdownToHTMLRenderer(\n            customBlockDirectives: [\n                MarkdownBlockDirective.Mocks.faq()\n            ]\n        )\n\n        let input = #\"\"\"\n            @unrecognized {\n                Lorem ipsum\n            }\n            \"\"\"#\n\n        let output = renderer.renderHTML(\n            markdown: input,\n            slug: \"\",\n            assetsPath: \"\",\n            baseURL: \"\"\n        )\n\n        #expect(output == \"\")\n    }\n\n    @Test\n    func parseError() throws {\n        let renderer = MarkdownToHTMLRenderer(\n            customBlockDirectives: [\n                MarkdownBlockDirective.Mocks.badDirective()\n            ]\n        )\n        let input = #\"\"\"\n            @BAD(columns: bad, columns: bad) {\n                Lorem ipsum \n            }\n            \"\"\"#\n\n        _ = renderer.renderHTML(\n            markdown: input,\n            slug: \"\",\n            assetsPath: \"\",\n            baseURL: \"\"\n        )\n    }\n\n    @Test\n    func requiredParameterErrors() throws {\n        let renderer = MarkdownToHTMLRenderer(\n            customBlockDirectives: [\n                MarkdownBlockDirective.Mocks.badDirective()\n            ]\n        )\n        let input = #\"\"\"\n            @BAD() {\n                Lorem ipsum \n            }\n            \"\"\"#\n\n        _ = renderer.renderHTML(\n            markdown: input,\n            slug: \"\",\n            assetsPath: \"\",\n            baseURL: \"\"\n        )\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanMarkdownTests/OutlineTestSuite.swift",
    "content": "//\n//  OutlineTestSuite.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 02. 20..\n\nimport Testing\n\n@testable import ToucanMarkdown\n\n@Suite\nstruct ToucanToCTestSuite {\n    @Test\n    func withoutFragments() async throws {\n        let html = #\"\"\"\n            <h1>Lorem ipsum</h1>\n            <p>lorem ipsum dolor sit amet</p>\n\n                <h2>Dolor sit</h2>\n                <p>lorem ipsum dolor sit amet</p>\n\n                    <h3>Amet</h3>\n                    <p>lorem ipsum dolor sit amet</p>\n\n                <h2>Hello world</h2>\n                <p>lorem ipsum dolor sit amet</p>\n\n                    <h3>foo, bar, baz</h3>\n                    <p>lorem ipsum dolor sit amet</p>\n            \"\"\"#\n\n        let parser = OutlineParser(levels: [2, 3])\n\n        let toc = parser.parseHTML(html)\n        #expect(toc.count == 4)\n    }\n\n    @Test\n    func example() async throws {\n        let html = #\"\"\"\n            <h1>Lorem ipsum</h1>\n            <p>lorem ipsum dolor sit amet</p>\n\n                <h2 id=\"dolor-sit\">Dolor sit</h2>\n                <p>lorem ipsum dolor sit amet</p>\n\n                    <h3 id=\"amet\">Amet</h3>\n                    <p>lorem ipsum dolor sit amet</p>\n\n                <h2 id=\"hello-world\">Hello world</h2>\n                <p>lorem ipsum dolor sit amet</p>\n\n                    <h3 id=\"foo-bar-baz\">foo, bar, baz</h3>\n                    <p>lorem ipsum dolor sit amet</p>\n            \"\"\"#\n\n        let parser = OutlineParser()\n\n        let toc = parser.parseHTML(html)\n        #expect(toc.count == 5)\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSDKTests/BuildTargetSource/BuildTargetSourceRendererTestSuite.swift",
    "content": "//\n//  BuildTargetSourceRendererTestSuite.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 06. 09..\n//\n\nimport Foundation\nimport Logging\nimport Testing\nimport ToucanCore\n@testable import ToucanSDK\nimport ToucanSource\nimport ToucanSerialization\n\n@Suite\nstruct BuildTargetSourceRendererTestSuite {\n\n    func renderBlock(\n        pipeline: Pipeline,\n        contextBundles: [ContextBundle]\n    ) throws -> [PipelineResult] {\n        let logger = Logger(label: \"test\")\n\n        switch pipeline.engine.id {\n        case \"json\":\n            let renderer = ContextBundleToJSONRenderer(\n                pipeline: pipeline,\n                logger: logger\n            )\n            return renderer.render(contextBundles)\n        case \"mustache\":\n            let templateLoader = TemplateLoader(\n                locations: .init(\n                    sourceURL: .init(filePath: \"\"),\n                    config: .defaults\n                ),\n                fileManager: FileManager.default,\n                encoder: ToucanYAMLEncoder(),\n                decoder: ToucanYAMLDecoder(),\n                logger: logger\n            )\n            let template = try templateLoader.load()\n\n            let templateValidator = try TemplateValidator(\n                generatorInfo: .current\n            )\n            try templateValidator.validate(template)\n\n            let renderer = try ContextBundleToHTMLRenderer(\n                pipeline: pipeline,\n                templates: template.getViewIDsWithContents(),\n                logger: logger\n            )\n            return renderer.render(contextBundles)\n        default:\n            throw BuildTargetSourceRendererError.invalidEngine(\n                pipeline.engine.id\n            )\n        }\n    }\n\n    // MARK: - api\n\n    private func getMockAPIBuildTargetSource(\n        now: Date,\n        options: [String: AnyCodable]\n    ) -> BuildTargetSource {\n        let pipelines: [Pipeline] = [\n            .init(\n                id: \"api\",\n                definesType: true,\n                scopes: [:],\n                queries: [\n                    \"posts\": .init(\n                        contentType: \"post\",\n                        scope: Pipeline.Scope.Keys.list.rawValue,\n                        orderBy: [\n                            .init(\n                                key: \"publication\",\n                                direction: .desc\n                            )\n                        ]\n                    )\n                ],\n                dataTypes: .defaults,\n                contentTypes: .init(\n                    include: [\"api\"],\n                    exclude: [],\n                    lastUpdate: [],\n                    filterRules: [:]\n                ),\n                iterators: [\n                    \"api.posts.pagination\": .init(\n                        contentType: \"post\",\n                        limit: 2\n                    )\n                ],\n                assets: .defaults,\n                transformers: [:],\n                engine: .init(\n                    id: \"json\",\n                    options: options\n                ),\n                output: .init(\n                    path: \"\",\n                    file: \"{{slug}}\",\n                    ext: \"json\"\n                )\n            )\n        ]\n\n        let rawContents: [RawContent] = [\n            .init(\n                origin: .init(\n                    path: .init(\"api\"),\n                    slug: \"api\"\n                ),\n                markdown: .init(\n                    frontMatter: [\n                        \"type\": \"api\"\n                    ]\n                ),\n                lastModificationDate: now.timeIntervalSince1970,\n                assetsPath: \"assets\",\n                assets: []\n            ),\n            .init(\n                origin: .init(\n                    path: .init(\"api/posts/{{api.posts.pagination}}\"),\n                    slug: \"api/posts/{{api.posts.pagination}}\"\n                ),\n                markdown: .init(\n                    frontMatter: [\n                        \"type\": \"api\"\n                    ]\n                ),\n                lastModificationDate: now.timeIntervalSince1970,\n                assetsPath: \"assets\",\n                assets: []\n            ),\n        ]\n\n        var buildTargetSource = Mocks.buildTargetSource(now: now)\n        // keep only api pipeline, exclude sitemap & rss xml contents\n        buildTargetSource.pipelines = pipelines\n        buildTargetSource.rawContents =\n            buildTargetSource.rawContents.filter {\n                !$0.origin.path.value.hasSuffix(\"xml\")\n                    && !$0.origin.path.value.contains(\"404\")\n            } + rawContents\n\n        return buildTargetSource\n    }\n\n    @Test\n    func emptyContentTypes() throws {\n        let now = Date()\n        let buildTargetSource = BuildTargetSource(\n            locations: .init(\n                sourceURL: .init(filePath: \"\"),\n                config: .defaults\n            )\n        )\n\n        var renderer = BuildTargetSourceRenderer(\n            buildTargetSource: buildTargetSource\n        )\n\n        _ = try renderer.render(now: now) {\n            try renderBlock(pipeline: $0, contextBundles: $1)\n        }\n    }\n\n    @Test\n    func wrongRendererEngine() throws {\n        let now = Date()\n        let buildTargetSource = BuildTargetSource(\n            locations: .init(\n                sourceURL: .init(filePath: \"\"),\n                config: .defaults\n            ),\n            pipelines: [\n                .init(\n                    id: \"test\",\n                    definesType: false,\n                    scopes: [:],\n                    queries: [:],\n                    dataTypes: .defaults,\n                    contentTypes: .defaults,\n                    iterators: [:],\n                    assets: .defaults,\n                    transformers: [:],\n                    engine: .init(id: \"wrong\", options: [:]),\n                    output: .init(path: \"\", file: \"context\", ext: \"json\")\n                )\n            ]\n        )\n\n        var renderer = BuildTargetSourceRenderer(\n            buildTargetSource: buildTargetSource\n        )\n\n        do {\n            _ = try renderer.render(now: now) {\n                try renderBlock(pipeline: $0, contextBundles: $1)\n            }\n        }\n        catch let error as BuildTargetSourceRendererError {\n            switch error {\n            case let .invalidEngine(id):\n                #expect(id == \"wrong\")\n            default:\n                Issue.record(\"\\(error.logMessage)\")\n            }\n        }\n    }\n\n    @Test()\n    func generatorMetadata() async throws {\n        let now = Date()\n        let config = Config.defaults\n        let target = Target.standard\n        let settings = Settings.defaults\n\n        let dateFormatter = ToucanOutputDateFormatter(\n            dateConfig: config.dataTypes.date\n        )\n        let nowISO8601String = dateFormatter.format(now).iso8601\n\n        let pipelines: [Pipeline] = [\n            .init(\n                id: \"test\",\n                definesType: false,\n                scopes: [:],\n                queries: [:],\n                dataTypes: .defaults,\n                contentTypes: .defaults,\n                iterators: [:],\n                assets: .defaults,\n                transformers: [:],\n                engine: .init(id: \"json\", options: [:]),\n                output: .init(path: \"\", file: \"context\", ext: \"json\")\n            )\n        ]\n\n        let rawContents: [RawContent] = [\n            .init(\n                origin: .init(\n                    path: .init(\"\"),\n                    slug: \"\"\n                ),\n                markdown: .init(\n                    frontMatter: [\n                        \"title\": \"Home\",\n                        \"description\": \"Home description\",\n                        \"foo\": [\n                            \"bar\": \"baz\"\n                        ],\n                    ],\n                    contents: \"\"\"\n                        # Home\n\n                        Lorem ipsum dolor sit amet\n                        \"\"\"\n                ),\n                lastModificationDate: now.timeIntervalSince1970,\n                assetsPath: \"assets\",\n                assets: []\n            )\n        ]\n\n        let buildTargetSource = BuildTargetSource(\n            locations: .init(\n                sourceURL: .init(filePath: \"\"),\n                config: config\n            ),\n            target: target,\n            config: config,\n            settings: settings,\n            pipelines: pipelines,\n            types: [\n                Mocks.ContentTypes.page()\n            ],\n            rawContents: rawContents,\n            blockDirectives: []\n        )\n\n        var renderer = BuildTargetSourceRenderer(\n            buildTargetSource: buildTargetSource\n        )\n        let results = try renderer.render(now: now) {\n            try renderBlock(pipeline: $0, contextBundles: $1)\n        }\n\n        #expect(results.count == 1)\n        guard case let .content(value) = results[0].source else {\n            Issue.record(\"Source type is not a valid content.\")\n            return\n        }\n\n        let decoder = JSONDecoder()\n\n        struct Exp: Decodable {\n            struct Site: Codable {}\n\n            let site: Site\n            let generation: DateContext\n            let generator: GeneratorInfo\n        }\n\n        let data = try #require(value.data(using: .utf8))\n        let exp = try decoder.decode(Exp.self, from: data)\n        let info = GeneratorInfo.current\n\n        #expect(exp.generator.name == info.name)\n        #expect(exp.generator.release == info.release)\n        #expect(exp.generation.iso8601 == nowISO8601String)\n    }\n\n    @Test()\n    func pipelineContentFilter() async throws {\n        let now = Date()\n\n        let pipelines: [Pipeline] = [\n            .init(\n                id: \"test\",\n                definesType: false,\n                scopes: [:],\n                queries: [:],\n                dataTypes: .defaults,\n                contentTypes: .init(\n                    include: [\n                        \"test\"\n                    ],\n                    exclude: [],\n                    lastUpdate: [],\n                    filterRules: [\n                        \"*\": .field(\n                            key: \"title\",\n                            operator: .equals,\n                            value: \"foo\"\n                        )\n                    ]\n                ),\n                iterators: [:],\n                assets: .defaults,\n                transformers: [:],\n                engine: .init(id: \"json\", options: [:]),\n                output: .init(path: \"\", file: \"context\", ext: \"json\")\n            )\n        ]\n\n        let rawContents: [RawContent] = [\n            .init(\n                origin: .init(\n                    path: .init(\"\"),\n                    slug: \"\"\n                ),\n                markdown: .init(\n                    frontMatter: [\n                        \"type\": \"test\",\n                        \"title\": \"Home\",\n                        \"description\": \"Home description\",\n                        \"foo\": [\n                            \"bar\": \"baz\"\n                        ],\n                    ],\n                    contents: \"\"\"\n                        # Home\n\n                        Lorem ipsum dolor sit amet\n                        \"\"\"\n                ),\n                lastModificationDate: now.timeIntervalSince1970,\n                assetsPath: \"assets\",\n                assets: []\n            )\n        ]\n\n        let types: [ContentType] = [\n            .init(\n                id: \"test\",\n                default: true,\n                paths: [],\n                properties: [\n                    \"title\": .init(\n                        propertyType: .string,\n                        isRequired: true\n                    )\n                ],\n                relations: [:],\n                queries: [:]\n            )\n        ]\n\n        var buildTargetSource = Mocks.buildTargetSource(now: now)\n        buildTargetSource.pipelines = pipelines\n        buildTargetSource.rawContents = rawContents\n        buildTargetSource.types = types\n\n        var renderer = BuildTargetSourceRenderer(\n            buildTargetSource: buildTargetSource\n        )\n        let results = try renderer.render(now: now) {\n            try renderBlock(pipeline: $0, contextBundles: $1)\n        }\n\n        #expect(results.isEmpty)\n    }\n\n    @Test()\n    func renderAPIBasics() async throws {\n        let now = Date()\n        let buildTargetSource = getMockAPIBuildTargetSource(\n            now: now,\n            options: [:]\n        )\n\n        var renderer = BuildTargetSourceRenderer(\n            buildTargetSource: buildTargetSource\n        )\n        let results = try renderer.render(now: now) {\n            try renderBlock(pipeline: $0, contextBundles: $1)\n        }\n\n        #expect(results.count == 3)\n\n        let contents =\n            results\n            .filter(\\.source.isContent)\n            .filter { $0.destination.file == \"api\" }\n\n        #expect(contents.count == 1)\n\n        guard\n            case let .content(value) = contents[0].source,\n            let data = value.data(using: .utf8)\n        else {\n            Issue.record(\"Source type is not a valid content.\")\n            return\n        }\n\n        struct Expected: Decodable {\n            struct Item: Decodable {\n                let title: String\n                let slug: Slug\n            }\n\n            struct Context: Decodable {\n                let posts: [Item]\n            }\n\n            let context: Context\n        }\n\n        let decoder = JSONDecoder()\n        let result = try decoder.decode(Expected.self, from: data)\n        #expect(result.context.posts.count == 3)\n    }\n\n    @Test()\n    func renderAPIPagination() async throws {\n        let now = Date()\n        let buildTargetSource = getMockAPIBuildTargetSource(\n            now: now,\n            options: [:]\n        )\n\n        var renderer = BuildTargetSourceRenderer(\n            buildTargetSource: buildTargetSource\n        )\n        let results = try renderer.render(now: now) {\n            try renderBlock(pipeline: $0, contextBundles: $1)\n        }\n\n        #expect(results.count == 3)\n\n        let contents = results.filter(\\.source.isContent)\n            .filter { $0.destination.file != \"api\" }\n\n        #expect(contents.count == 2)\n\n        for content in contents {\n            guard\n                case let .content(value) = content.source,\n                let data = value.data(using: .utf8)\n            else {\n                Issue.record(\"Source type is not a valid content.\")\n                return\n            }\n\n            struct Expected: Decodable {\n                struct Item: Decodable {\n                    let title: String\n                    let slug: Slug\n                }\n\n                struct Iterator: Decodable {\n                    let current: Int\n                    let items: [Item]\n                }\n\n                let iterator: Iterator\n            }\n\n            let decoder = JSONDecoder()\n\n            let result = try decoder.decode(Expected.self, from: data)\n            switch result.iterator.current {\n            case 1:\n                #expect(result.iterator.items.count == 2)\n            case 2:\n                #expect(result.iterator.items.count == 1)\n            default:\n                Issue.record(\"Invalid iterator page.\")\n            }\n        }\n    }\n\n    @Test()\n    func renderAPIWithEngineOptionsKeyPath() async throws {\n        let now = Date()\n        let buildTargetSource = getMockAPIBuildTargetSource(\n            now: now,\n            options: [\n                \"keyPath\": \"context.posts\"\n            ]\n        )\n\n        var renderer = BuildTargetSourceRenderer(\n            buildTargetSource: buildTargetSource\n        )\n        let results = try renderer.render(now: now) {\n            try renderBlock(pipeline: $0, contextBundles: $1)\n        }\n\n        #expect(results.count == 3)\n\n        let contents = results.filter(\\.source.isContent)\n            .filter { $0.destination.file == \"api\" }\n\n        #expect(contents.count == 1)\n\n        guard\n            case let .content(value) = contents[0].source,\n            let data = value.data(using: .utf8)\n        else {\n            Issue.record(\"Source type is not a valid content.\")\n            return\n        }\n\n        struct Expected: Decodable {\n            let title: String\n            let slug: Slug\n        }\n\n        let decoder = JSONDecoder()\n        let result = try decoder.decode([Expected].self, from: data)\n        #expect(result.count == 3)\n    }\n\n    @Test()\n    func renderAPIWithEngineOptionsMultipleKeyPaths() async throws {\n        let now = Date()\n        let buildTargetSource = getMockAPIBuildTargetSource(\n            now: now,\n            options: [\n                \"keyPaths\": [\n                    \"context.posts\": \"items\",\n                    \"generator\": \"info\",\n                ]\n            ]\n        )\n\n        var renderer = BuildTargetSourceRenderer(\n            buildTargetSource: buildTargetSource\n        )\n        let results = try renderer.render(now: now) {\n            try renderBlock(pipeline: $0, contextBundles: $1)\n        }\n\n        let contents = results.filter(\\.source.isContent)\n            .filter { $0.destination.file == \"api\" }\n\n        #expect(contents.count == 1)\n\n        guard\n            case let .content(value) = contents[0].source,\n            let data = value.data(using: .utf8)\n        else {\n            Issue.record(\"Source type is not a valid content.\")\n            return\n        }\n\n        struct Expected: Decodable {\n            struct Item: Decodable {\n                let title: String\n                let slug: Slug\n            }\n\n            struct Info: Decodable {\n                let name: String\n                let release: String\n            }\n\n            let items: [Item]\n            let info: Info\n        }\n\n        let decoder = JSONDecoder()\n        let result = try decoder.decode(Expected.self, from: data)\n\n        #expect(result.items.count == 3)\n        #expect(result.info.name == \"Toucan\")\n        #expect(\n            result.info.release == GeneratorInfo.current.release.description\n        )\n    }\n\n    // MARK: - contents\n\n    @Test()\n    func renderAuthor() async throws {\n        let now = Date()\n\n        var buildTargetSource = Mocks.buildTargetSource(now: now)\n        // keep only html pipeline & one author\n        buildTargetSource.pipelines = buildTargetSource.pipelines.filter {\n            $0.id == \"html\"\n        }\n        buildTargetSource.rawContents = buildTargetSource.rawContents.filter {\n            $0.origin.slug == \"blog/authors/author-1\"\n        }\n\n        var renderer = BuildTargetSourceRenderer(\n            buildTargetSource: buildTargetSource\n        )\n        let results = try renderer.render(now: now) {\n            // try renderBlock(pipeline: $0, contextBundles: $1)\n            /// We provide the template manually for this test. Skipping parsing and validation.\n            let template = Mocks.Templates.example()\n            let renderer = try ContextBundleToHTMLRenderer(\n                pipeline: $0,\n                templates: template.getViewIDsWithContents(),\n                logger: .init(label: \"test\")\n            )\n            return renderer.render($1)\n        }\n\n        #expect(results.count == 2)\n\n        let contents = results.filter(\\.source.isContent)\n        #expect(contents.count == 1)\n\n        let assets = results.filter(\\.source.isAsset)\n        #expect(assets.count == 1)\n\n        guard case let .content(value) = contents[0].source else {\n            Issue.record(\"Source type is not a valid content.\")\n            return\n        }\n        #expect(!value.contains(\"./assets\"))\n\n        guard case let .assetFile(path) = assets[0].source else {\n            Issue.record(\"Source type is not a valid asset file.\")\n            return\n        }\n        #expect(path == \"blog/authors/author-1/assets/author-1.jpg\")\n    }\n\n    // MARK: - assets\n\n    @Test()\n    func assetPropertyAddOne() async throws {\n        let now = Date()\n\n        let pipelines: [Pipeline] = [\n            .init(\n                id: \"test\",\n                definesType: true,\n                scopes: [:],\n                queries: [:],\n                dataTypes: .defaults,\n                contentTypes: .defaults,\n                iterators: [:],\n                assets: .init(\n                    behaviors: [],\n                    properties: [\n                        .init(\n                            action: .add,\n                            property: \"css\",\n                            resolvePath: true,\n                            input: .init(\n                                path: nil,\n                                name: \"style\",\n                                ext: \"css\"\n                            )\n                        )\n                    ]\n                ),\n                transformers: [:],\n                engine: .init(\n                    id: \"json\",\n                    options: [\n                        \"keyPath\": \"page\"\n                    ]\n                ),\n                output: .init(path: \"\", file: \"context\", ext: \"json\")\n            )\n        ]\n\n        let rawContents: [RawContent] = [\n            .init(\n                origin: .init(\n                    path: .init(\"test\"),\n                    slug: \"test\"\n                ),\n                markdown: .init(\n                    frontMatter: [\n                        \"type\": \"test\",\n                        \"css\": [\n                            \"https://test.css\"\n                        ],\n                    ]\n                ),\n                lastModificationDate: now.timeIntervalSince1970,\n                assetsPath: \"assets\",\n                assets: [\n                    \"style.css\"\n                ]\n            )\n        ]\n\n        let types: [ContentType] = []\n\n        var buildTargetSource = Mocks.buildTargetSource(now: now)\n        buildTargetSource.pipelines = pipelines\n        buildTargetSource.rawContents = rawContents\n        buildTargetSource.types = types\n\n        var renderer = BuildTargetSourceRenderer(\n            buildTargetSource: buildTargetSource\n        )\n        let results = try renderer.render(now: now) {\n            try renderBlock(pipeline: $0, contextBundles: $1)\n        }\n\n        #expect(results.count == 1)\n\n        let contents = results.filter(\\.source.isContent)\n        #expect(contents.count == 1)\n\n        guard case let .content(value) = contents[0].source else {\n            Issue.record(\"Source type is not a valid content.\")\n            return\n        }\n\n        let decoder = JSONDecoder()\n\n        struct Exp: Decodable {\n            let css: [String]\n        }\n\n        let data = try #require(value.data(using: .utf8))\n        let exp = try decoder.decode(Exp.self, from: data)\n\n        #expect(\n            exp.css.sorted()\n                == [\n                    \"https://test.css\",\n                    \"http://localhost:3000/assets/test/style.css\",\n                ]\n                .sorted()\n        )\n    }\n\n    @Test()\n    func assetPropertyAddMultiple() async throws {\n        let now = Date()\n\n        let pipelines: [Pipeline] = [\n            .init(\n                id: \"test\",\n                definesType: true,\n                scopes: [:],\n                queries: [:],\n                dataTypes: .defaults,\n                contentTypes: .defaults,\n                iterators: [:],\n                assets: .init(\n                    behaviors: [],\n                    properties: [\n                        .init(\n                            action: .add,\n                            property: \"css\",\n                            resolvePath: false,\n                            input: .init(\n                                path: nil,\n                                name: \"*\",\n                                ext: \"css\"\n                            )\n                        )\n                    ]\n                ),\n                transformers: [:],\n                engine: .init(\n                    id: \"json\",\n                    options: [\n                        \"keyPath\": \"page\"\n                    ]\n                ),\n                output: .init(path: \"\", file: \"context\", ext: \"json\")\n            )\n        ]\n\n        let rawContents: [RawContent] = [\n            .init(\n                origin: .init(\n                    path: .init(\"test\"),\n                    slug: \"test\"\n                ),\n                markdown: .init(\n                    frontMatter: [\n                        \"type\": \"test\",\n                        \"css\": [\n                            \"https://test.css\"\n                        ],\n                    ]\n                ),\n                lastModificationDate: now.timeIntervalSince1970,\n                assetsPath: \"assets\",\n                assets: [\n                    \"foo.css\",\n                    \"bar.css\",\n                ]\n            )\n        ]\n\n        let types: [ContentType] = []\n\n        var buildTargetSource = Mocks.buildTargetSource(now: now)\n        buildTargetSource.pipelines = pipelines\n        buildTargetSource.rawContents = rawContents\n        buildTargetSource.types = types\n\n        var renderer = BuildTargetSourceRenderer(\n            buildTargetSource: buildTargetSource\n        )\n        let results = try renderer.render(now: now) {\n            try renderBlock(pipeline: $0, contextBundles: $1)\n        }\n\n        #expect(results.count == 1)\n\n        let contents = results.filter(\\.source.isContent)\n        #expect(contents.count == 1)\n\n        guard case let .content(value) = contents[0].source else {\n            Issue.record(\"Source type is not a valid content.\")\n            return\n        }\n\n        let decoder = JSONDecoder()\n\n        struct Exp: Decodable {\n            let css: [String]\n        }\n\n        let data = try #require(value.data(using: .utf8))\n        let exp = try decoder.decode(Exp.self, from: data)\n\n        #expect(\n            exp.css.sorted()\n                == [\n                    \"https://test.css\",\n                    \"foo.css\",\n                    \"bar.css\",\n                ]\n                .sorted()\n        )\n    }\n\n    @Test()\n    func assetPropertySetOne() async throws {\n        let now = Date()\n\n        let pipelines: [Pipeline] = [\n            .init(\n                id: \"test\",\n                definesType: true,\n                scopes: [:],\n                queries: [:],\n                dataTypes: .defaults,\n                contentTypes: .defaults,\n                iterators: [:],\n                assets: .init(\n                    behaviors: [],\n                    properties: [\n                        .init(\n                            action: .set,\n                            property: \"image\",\n                            resolvePath: true,\n                            input: .init(\n                                path: nil,\n                                name: \"cover\",\n                                ext: \"jpg\"\n                            )\n                        )\n                    ]\n                ),\n                transformers: [:],\n                engine: .init(\n                    id: \"json\",\n                    options: [\n                        \"keyPath\": \"page\"\n                    ]\n                ),\n                output: .init(path: \"\", file: \"context\", ext: \"json\")\n            )\n        ]\n\n        let rawContents: [RawContent] = [\n            .init(\n                origin: .init(\n                    path: .init(\"test\"),\n                    slug: \"test\"\n                ),\n                markdown: .init(\n                    frontMatter: [\n                        \"type\": \"test\"\n                    ]\n                ),\n                lastModificationDate: now.timeIntervalSince1970,\n                assetsPath: \"assets\",\n                assets: [\n                    \"cover.jpg\"\n                ]\n            )\n        ]\n\n        let types: [ContentType] = []\n\n        var buildTargetSource = Mocks.buildTargetSource(now: now)\n        buildTargetSource.pipelines = pipelines\n        buildTargetSource.rawContents = rawContents\n        buildTargetSource.types = types\n\n        var renderer = BuildTargetSourceRenderer(\n            buildTargetSource: buildTargetSource\n        )\n        let results = try renderer.render(now: now) {\n            try renderBlock(pipeline: $0, contextBundles: $1)\n        }\n\n        #expect(results.count == 1)\n\n        let contents = results.filter(\\.source.isContent)\n        #expect(contents.count == 1)\n\n        guard case let .content(value) = contents[0].source else {\n            Issue.record(\"Source type is not a valid content.\")\n            return\n        }\n\n        let decoder = JSONDecoder()\n\n        struct Exp: Decodable {\n            let image: String\n        }\n\n        let data = try #require(value.data(using: .utf8))\n        let exp = try decoder.decode(Exp.self, from: data)\n\n        #expect(exp.image == \"http://localhost:3000/assets/test/cover.jpg\")\n    }\n\n    @Test()\n    func assetPropertySetMultiple() async throws {\n        let now = Date()\n\n        let pipelines: [Pipeline] = [\n            .init(\n                id: \"test\",\n                definesType: true,\n                scopes: [:],\n                queries: [:],\n                dataTypes: .defaults,\n                contentTypes: .defaults,\n                iterators: [:],\n                assets: .init(\n                    behaviors: [],\n                    properties: [\n                        .init(\n                            action: .set,\n                            property: \"images\",\n                            resolvePath: true,\n                            input: .init(\n                                path: nil,\n                                name: \"*\",\n                                ext: \"png\"\n                            )\n                        )\n                    ]\n                ),\n                transformers: [:],\n                engine: .init(\n                    id: \"json\",\n                    options: [\n                        \"keyPath\": \"page\"\n                    ]\n                ),\n                output: .init(path: \"\", file: \"context\", ext: \"json\")\n            )\n        ]\n\n        let rawContents: [RawContent] = [\n            .init(\n                origin: .init(\n                    path: .init(\"test\"),\n                    slug: \"test\"\n                ),\n                markdown: .init(\n                    frontMatter: [\n                        \"type\": \"test\"\n                    ]\n                ),\n                lastModificationDate: now.timeIntervalSince1970,\n                assetsPath: \"assets\",\n                assets: [\n                    \"cover.jpg\",\n                    \"foo.png\",\n                    \"bar.png\",\n                ]\n            )\n        ]\n\n        let types: [ContentType] = []\n\n        var buildTargetSource = Mocks.buildTargetSource(now: now)\n        buildTargetSource.pipelines = pipelines\n        buildTargetSource.rawContents = rawContents\n        buildTargetSource.types = types\n\n        var renderer = BuildTargetSourceRenderer(\n            buildTargetSource: buildTargetSource\n        )\n        let results = try renderer.render(now: now) {\n            try renderBlock(pipeline: $0, contextBundles: $1)\n        }\n\n        #expect(results.count == 1)\n\n        let contents = results.filter(\\.source.isContent)\n        #expect(contents.count == 1)\n\n        guard case let .content(value) = contents[0].source else {\n            Issue.record(\"Source type is not a valid content.\")\n            return\n        }\n\n        let decoder = JSONDecoder()\n\n        struct Exp: Decodable {\n            let images: [String: String]\n        }\n\n        let data = try #require(value.data(using: .utf8))\n        let exp = try decoder.decode(Exp.self, from: data)\n\n        #expect(exp.images.keys.sorted() == [\"foo\", \"bar\"].sorted())\n        #expect(\n            exp.images.values.sorted()\n                == [\n                    \"http://localhost:3000/assets/test/foo.png\",\n                    \"http://localhost:3000/assets/test/bar.png\",\n                ]\n                .sorted()\n        )\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSDKTests/BuildTargetSource/BuildTargetSourceValidatorTestSuite.swift",
    "content": "//\n//  BuildTargetSourceValidatorTestSuite.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 23..\n//\n//\nimport Foundation\nimport Logging\nimport Testing\nimport ToucanCore\n@testable import ToucanSDK\nimport ToucanSource\n\n@Suite\nstruct BuildTargetSourceValidatorTestSuite {\n\n    @Test\n    func emptyContentTypes() throws {\n        let buildTargetSource = BuildTargetSource(\n            locations: .init(\n                sourceURL: .init(filePath: \"\"),\n                config: .defaults\n            )\n        )\n\n        let validator = BuildTargetSourceValidator(\n            buildTargetSource: buildTargetSource\n        )\n\n        do {\n            try validator.validate()\n            Issue.record(\"Should trigger an error.\")\n        }\n        catch {\n            guard case .noDefaultContentType = error else {\n                Issue.record(\"Invalid error.\")\n                return\n            }\n        }\n    }\n\n    @Test\n    func noDefaultContentType() throws {\n        let buildTargetSource = BuildTargetSource(\n            locations: .init(\n                sourceURL: .init(filePath: \"\"),\n                config: .defaults\n            ),\n            types: [\n                .init(id: \"foo\"),\n                .init(id: \"bar\"),\n            ]\n        )\n\n        let validator = BuildTargetSourceValidator(\n            buildTargetSource: buildTargetSource\n        )\n\n        do {\n            try validator.validate()\n            Issue.record(\"Should trigger an error.\")\n        }\n        catch {\n            guard case .noDefaultContentType = error else {\n                Issue.record(\"Invalid error.\")\n                return\n            }\n        }\n    }\n\n    @Test\n    func multipleDefaultContentTypes() throws {\n        let buildTargetSource = BuildTargetSource(\n            locations: .init(\n                sourceURL: .init(filePath: \"\"),\n                config: .defaults\n            ),\n            types: [\n                .init(\n                    id: \"foo\",\n                    default: true\n                ),\n                .init(\n                    id: \"bar\",\n                    default: true\n                ),\n            ]\n        )\n\n        let validator = BuildTargetSourceValidator(\n            buildTargetSource: buildTargetSource\n        )\n\n        do {\n            try validator.validate()\n            Issue.record(\"Should trigger an error.\")\n        }\n        catch {\n            guard case let .multipleDefaultContentTypes(values) = error else {\n                Issue.record(\"Invalid error.\")\n                return\n            }\n            #expect(values == [\"foo\", \"bar\"].sorted())\n        }\n    }\n\n    @Test\n    func invalidOriginPath() throws {\n        let buildTargetSource = BuildTargetSource(\n            locations: .init(\n                sourceURL: .init(filePath: \"\"),\n                config: .defaults\n            ),\n            types: [\n                .init(\n                    id: \"page\",\n                    default: true\n                )\n            ],\n            rawContents: [\n                .init(\n                    origin: .init(\n                        path: .init(\"foo?bar\"),\n                        slug: \"foo-bar\"\n                    ),\n                    lastModificationDate: Date().timeIntervalSinceNow,\n                    assetsPath: \"\",\n                    assets: []\n                )\n            ]\n        )\n\n        let validator = BuildTargetSourceValidator(\n            buildTargetSource: buildTargetSource\n        )\n\n        do {\n            try validator.validate()\n            Issue.record(\"Should trigger an error.\")\n        }\n        catch {\n            guard case let .invalidRawContentOriginPath(path) = error else {\n                Issue.record(\"Invalid error.\")\n                return\n            }\n            #expect(path == \"foo?bar\")\n        }\n    }\n\n}\n"
  },
  {
    "path": "Tests/ToucanSDKTests/Content/ContentQueryTestSuite.swift",
    "content": "//\n//  ContentQueryTestSuite.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 04. 15..\n//\nimport Foundation\nimport Testing\nimport ToucanCore\n@testable import ToucanSDK\nimport ToucanSerialization\nimport ToucanSource\n\n@Suite\nstruct ContentQueryTestSuite {\n    func getMockContents(now: Date) throws -> [Content] {\n        let buildTargetSource = Mocks.buildTargetSource(now: now)\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n\n        let converter = ContentResolver(\n            contentTypeResolver: .init(\n                types: buildTargetSource.types,\n                pipelines: buildTargetSource.pipelines\n            ),\n            encoder: encoder,\n            decoder: decoder,\n            dateFormatter: .init(\n                dateConfig: buildTargetSource.config.dataTypes.date\n            )\n        )\n        return try converter.convert(\n            rawContents: buildTargetSource.rawContents\n        )\n    }\n\n    @Test\n    func limitOffsetOne() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"author\",\n            limit: 1,\n            offset: 1\n        )\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results.count == 1)\n        #expect(\n            results[0].properties[\"name\"]?.value(as: String.self) == \"Author #2\"\n        )\n    }\n\n    @Test\n    func limitOffsetTwo() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"author\",\n            limit: 2,\n            offset: 1\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results.count == 2)\n        #expect(\n            results[0].properties[\"name\"]?.value(as: String.self) == \"Author #2\"\n        )\n        #expect(\n            results[1].properties[\"name\"]?.value(as: String.self) == \"Author #3\"\n        )\n    }\n\n    @Test\n    func equalsFilterString() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"author\",\n            filter: .field(\n                key: \"name\",\n                operator: .equals,\n                value: .init(\"Author #3\")\n            )\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results.count == 1)\n        #expect(\n            results[0].properties[\"name\"]?.value(as: String.self) == \"Author #3\"\n        )\n    }\n\n    @Test\n    func filterInt() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"author\",\n            filter: .field(\n                key: \"age\",\n                operator: .greaterThan,\n                value: 22\n            )\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results.count == 1)\n        #expect(\n            results[0].properties[\"name\"]?.value(as: String.self) == \"Author #3\"\n        )\n    }\n\n    @Test\n    func filterDouble() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"author\",\n            filter: .field(\n                key: SystemPropertyKeys.lastUpdate.rawValue,\n                operator: .lessThanOrEquals,\n                value: .init(now.timeIntervalSince1970)\n            )\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results.count == 3)\n    }\n\n    @Test\n    func equalsFilterNoResults() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"author\",\n            filter: .field(\n                key: \"age\",\n                operator: .equals,\n                value: .init(666)\n            )\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results.count == 0)\n    }\n\n    @Test\n    func notEqualsFilter() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"author\",\n            filter: .field(\n                key: \"name\",\n                operator: .notEquals,\n                value: .init(\"Author #1\")\n            )\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results.count == 2)\n        #expect(\n            results[0].properties[\"name\"]?.value(as: String.self) == \"Author #2\"\n        )\n        #expect(\n            results[1].properties[\"name\"]?.value(as: String.self) == \"Author #3\"\n        )\n    }\n\n    @Test\n    func lessThanFilter() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"category\",\n            filter: .field(\n                key: \"order\",\n                operator: .lessThan,\n                value: .init(3)\n            )\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results.count == 2)\n        #expect(\n            results[0].properties[\"title\"]?.value(as: String.self)\n                == \"Category #1\"\n        )\n        #expect(\n            results[1].properties[\"title\"]?.value(as: String.self)\n                == \"Category #2\"\n        )\n    }\n\n    @Test\n    func lessThanOrEqualsFilterInt() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"category\",\n            filter: .field(\n                key: \"order\",\n                operator: .lessThanOrEquals,\n                value: .init(3)\n            )\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results.count == 3)\n        #expect(\n            results[0].properties[\"title\"]?.value(as: String.self)\n                == \"Category #1\"\n        )\n        #expect(\n            results[1].properties[\"title\"]?.value(as: String.self)\n                == \"Category #2\"\n        )\n        #expect(\n            results[2].properties[\"title\"]?.value(as: String.self)\n                == \"Category #3\"\n        )\n    }\n\n    @Test\n    func lessThanOrEqualsFilterDouble() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"post\",\n            filter: .field(\n                key: \"rating\",\n                operator: .lessThanOrEquals,\n                value: .init(1.5)\n            )\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results.count == 1)\n        #expect(\n            results[0].properties[\"title\"]?.value(as: String.self) == \"Post #1\"\n        )\n    }\n\n    @Test\n    func lessThanOrEqualsFilterString() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"author\",\n            filter: .field(\n                key: \"name\",\n                operator: .lessThanOrEquals,\n                value: .init(\"Author #2\")\n            )\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results.count == 2)\n        #expect(\n            results[0].properties[\"name\"]?.value(as: String.self)\n                == \"Author #1\"\n        )\n        #expect(\n            results[1].properties[\"name\"]?.value(as: String.self)\n                == \"Author #2\"\n        )\n    }\n\n    @Test\n    func greaterThanFilter() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"category\",\n            filter: .field(\n                key: \"order\",\n                operator: .greaterThan,\n                value: .init(2)\n            )\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results.count == 1)\n        #expect(\n            results[0].properties[\"title\"]?.value(as: String.self)\n                == \"Category #3\"\n        )\n    }\n\n    @Test\n    func greaterThanOrEqualsFilterInt() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"category\",\n            filter: .field(\n                key: \"order\",\n                operator: .greaterThanOrEquals,\n                value: .init(2)\n            )\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results.count == 2)\n        #expect(\n            results[0].properties[\"title\"]?.value(as: String.self)\n                == \"Category #2\"\n        )\n        #expect(\n            results[1].properties[\"title\"]?.value(as: String.self)\n                == \"Category #3\"\n        )\n    }\n\n    @Test\n    func greaterThanOrEqualsFilterDouble() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"post\",\n            filter: .field(\n                key: \"rating\",\n                operator: .greaterThanOrEquals,\n                value: .init(2.0)\n            )\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results.count == 2)\n        #expect(\n            results[0].properties[\"title\"]?.value(as: String.self)\n                == \"Post #2\"\n        )\n        #expect(\n            results[1].properties[\"title\"]?.value(as: String.self)\n                == \"Post #3\"\n        )\n    }\n\n    @Test\n    func greaterThanOrEqualsFilterString() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"author\",\n            filter: .field(\n                key: \"name\",\n                operator: .greaterThanOrEquals,\n                value: .init(\"Author #3\")\n            )\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results.count == 1)\n        #expect(\n            results[0].properties[\"name\"]?.value(as: String.self)\n                == \"Author #3\"\n        )\n    }\n\n    @Test\n    func greaterThanNoResult() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"author\",\n            filter: .field(\n                key: \"age\",\n                operator: .greaterThanOrEquals,\n                value: .init(\"value\")\n            )\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        #expect(results.count == 0)\n    }\n\n    @Test\n    func equalsFilterWithOrConditionAndOrderByDesc() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"author\",\n            filter: .or([\n                .field(\n                    key: \"name\",\n                    operator: .equals,\n                    value: .init(\"Author #1\")\n                ),\n                .field(\n                    key: \"name\",\n                    operator: .equals,\n                    value: .init(\"Author #3\")\n                ),\n            ]),\n            orderBy: [\n                .init(key: \"name\", direction: .desc)\n            ]\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results.count == 2)\n        #expect(\n            results[0].properties[\"name\"]?.value(as: String.self) == \"Author #3\"\n        )\n        #expect(\n            results[1].properties[\"name\"]?.value(as: String.self) == \"Author #1\"\n        )\n    }\n\n    @Test\n    func equalsFilterWithAndConditionEmptyresults() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"author\",\n            filter: .and([\n                .field(\n                    key: \"name\",\n                    operator: .equals,\n                    value: .init(\"Author 1\")\n                ),\n                .field(\n                    key: \"name\",\n                    operator: .equals,\n                    value: .init(\"Author 3\")\n                ),\n            ]),\n            orderBy: [\n                .init(key: \"name\", direction: .desc)\n            ]\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        #expect(results.isEmpty)\n    }\n\n    @Test\n    func equalsFilterWithAndConditionMultipleProperties() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"author\",\n            filter: .and([\n                .field(\n                    key: \"name\",\n                    operator: .equals,\n                    value: .init(\"Author #2\")\n                ),\n                .field(\n                    key: \"description\",\n                    operator: .like,\n                    value: .init(\"Author #2 desc\")\n                ),\n            ]),\n            orderBy: [\n                .init(key: \"name\", direction: .desc)\n            ]\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results.count == 1)\n        #expect(\n            results[0].properties[\"name\"]?.value(as: String.self) == \"Author #2\"\n        )\n    }\n\n    @Test\n    func equalsFilterWithInStringValue() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"author\",\n            filter: .field(\n                key: \"name\",\n                operator: .in,\n                value: .init([\"Author #2\", \"Author #3\"])\n            ),\n            orderBy: [\n                .init(key: \"name\")\n            ]\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results.count == 2)\n        #expect(\n            results[0].properties[\"name\"]?.value(as: String.self) == \"Author #2\"\n        )\n        #expect(\n            results[1].properties[\"name\"]?.value(as: String.self) == \"Author #3\"\n        )\n    }\n\n    @Test\n    func equalsFilterWithInIntValue() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"author\",\n            filter: .field(\n                key: \"age\",\n                operator: .in,\n                value: .init([21, 42])\n            ),\n            orderBy: [\n                .init(key: \"name\")\n            ]\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results.count == 2)\n        #expect(\n            results[0].properties[\"name\"]?.value(as: String.self) == \"Author #2\"\n        )\n        #expect(\n            results[1].properties[\"name\"]?.value(as: String.self) == \"Author #3\"\n        )\n    }\n\n    @Test\n    func equalsFilterWithIndoubleValue() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"post\",\n            filter: .field(\n                key: \"rating\",\n                operator: .in,\n                value: .init([1.0, 3.0])\n            ),\n            orderBy: [\n                .init(key: \"title\")\n            ]\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results.count == 2)\n        #expect(\n            results[0].properties[\"title\"]?.value(as: String.self) == \"Post #1\"\n        )\n        #expect(\n            results[1].properties[\"title\"]?.value(as: String.self) == \"Post #3\"\n        )\n    }\n\n    @Test\n    func likeFilter() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"author\",\n            filter: .field(\n                key: \"name\",\n                operator: .like,\n                value: .init(\"Auth\")\n            )\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results.count == 3)\n    }\n\n    @Test\n    func likeFilterWrongValue() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"author\",\n            filter: .field(\n                key: \"name\",\n                operator: .like,\n                value: .init(100)\n            )\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results.count == 0)\n    }\n\n    @Test\n    func caseInsensitiveLikeFilter() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"author\",\n            filter: .field(\n                key: \"name\",\n                operator: .caseInsensitiveLike,\n                value: .init(\"author #1\")\n            )\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results.count == 1)\n        #expect(\n            results[0].properties[\"name\"]?.value(as: String.self) == \"Author #1\"\n        )\n    }\n\n    @Test\n    func caseInsensitiveLikeFilterWrongValue() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"author\",\n            filter: .field(\n                key: \"name\",\n                operator: .caseInsensitiveLike,\n                value: .init(100)\n            )\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results.count == 0)\n    }\n\n    @Test\n    func containsStringValue() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"post\",\n            filter: .field(\n                key: \"authors\",\n                operator: .contains,\n                value: .init(\"author-1\")\n            )\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results.count == 2)\n    }\n\n    @Test\n    func equalsDoubleValue() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"post\",\n            filter: .field(\n                key: \"rating\",\n                operator: .equals,\n                value: .init(1.0)\n            )\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results.count == 1)\n    }\n\n    @Test\n    func inDoubleValue() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"post\",\n            filter: .field(\n                key: \"rating\",\n                operator: .in,\n                value: [2.0, 3.0]\n            )\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results.count == 2)\n    }\n\n    @Test\n    func containsNoValue() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"post\",\n            filter: .field(\n                key: \"rating\",\n                operator: .contains,\n                value: .init(666)\n            )\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results.count == 0)\n    }\n\n    @Test\n    func matchingWithString() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"post\",\n            filter: .field(\n                key: \"authors\",\n                operator: .matching,\n                value: [\"author-1\", \"author-2\"]\n            )\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results.count == 3)\n    }\n\n    @Test\n    func matching() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"post\",\n            filter: .field(\n                key: \"authors\",\n                operator: .matching,\n                value: [\"author-1\"]\n            )\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results.count == 2)\n    }\n\n    @Test\n    func matchingWithNoResult() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query = Query(\n            contentType: \"post\",\n            filter: .field(\n                key: \"authors\",\n                operator: .matching,\n                value: [\"author-4\"]\n            )\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results.count == 0)\n    }\n\n    @Test\n    func nextPost() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n        let pastDate =\n            now\n            .addingTimeInterval(-86400 * 2)\n            .addingTimeInterval(-1)\n\n        let query1 = Query(\n            contentType: \"post\",\n            filter: .field(\n                key: \"publication\",\n                operator: .greaterThan,\n                value: .init(pastDate.timeIntervalSince1970)\n            ),\n            orderBy: [\n                .init(\n                    key: \"publication\",\n                    direction: .asc\n                )\n            ]\n        )\n        let results1 = contents.run(\n            query: query1,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n\n        try #require(results1.count == 2)\n\n        #expect(\n            results1[0].properties[\"title\"]?.value(as: String.self) == \"Post #2\"\n        )\n        #expect(\n            results1[1].properties[\"title\"]?.value(as: String.self) == \"Post #1\"\n        )\n\n        let query = Query(\n            contentType: \"post\",\n            limit: 1,\n            filter: .field(\n                key: \"publication\",\n                operator: .greaterThan,\n                value: .init(pastDate.timeIntervalSince1970)\n            ),\n            orderBy: [\n                .init(\n                    key: \"publication\",\n                    direction: .desc\n                )\n            ]\n        )\n\n        let results2 = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n\n        try #require(results2.count == 1)\n\n        #expect(\n            results1[0].properties[\"title\"]?.value(as: String.self) == \"Post #2\"\n        )\n    }\n\n    @Test\n    func nextGuide() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query1 = Query(\n            contentType: \"guide\",\n            filter: .and(\n                [\n                    .field(\n                        key: \"order\",\n                        operator: .greaterThan,\n                        value: 8\n                    ),\n                    .field(\n                        key: \"category\",\n                        operator: .equals,\n                        value: \"category-3\"\n                    ),\n                ]\n            ),\n            orderBy: [\n                .init(\n                    key: \"order\",\n                    direction: .asc\n                )\n            ]\n        )\n\n        let results1 = contents.run(\n            query: query1,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results1.count == 1)\n    }\n\n    @Test\n    func resolveFilterParametersUsingID() async throws {\n        let now = Date()\n        let contents = try getMockContents(now: now)\n\n        let query1 = Query(\n            contentType: \"guide\",\n            filter: .field(\n                key: \"category\",\n                operator: .equals,\n                value: \"{{id}}\"\n            ),\n            orderBy: [\n                .init(\n                    key: \"order\",\n                    direction: .asc\n                )\n            ]\n        )\n        .resolveFilterParameters(\n            with: [\n                \"id\": \"category-1\"\n            ]\n        )\n\n        let results1 = contents.run(\n            query: query1,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentQueryTestSuite\")\n        )\n        try #require(results1.count == 3)\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSDKTests/Content/ContentResolverTestSuite.swift",
    "content": "//\n//  ContentResolverTestSuite.swift\n//  Toucan\n//\n//  Created by Viasz-Kádi Ferenc on 2025. 02. 20..\n//\n//\nimport Foundation\nimport Logging\nimport Testing\nimport ToucanCore\n@testable import ToucanSDK\nimport ToucanSerialization\nimport ToucanSource\n\n@Suite\nstruct ContentResolverTestSuite {\n    // MARK: -\n\n    private func getMockresolver(\n        buildTargetSource: BuildTargetSource,\n        now _: Date\n    ) throws -> ContentResolver {\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n        let dateFormatter = ToucanInputDateFormatter(\n            dateConfig: buildTargetSource.config.dataTypes.date\n        )\n\n        return .init(\n            contentTypeResolver: .init(\n                types: buildTargetSource.types,\n                pipelines: buildTargetSource.pipelines\n            ),\n            encoder: encoder,\n            decoder: decoder,\n            dateFormatter: dateFormatter\n        )\n    }\n\n    @Test\n    func contentBasicConversion() throws {\n        let now = Date()\n        let buildTargetSource = Mocks.buildTargetSource(now: now)\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n        let dateFormatter = ToucanInputDateFormatter(\n            dateConfig: buildTargetSource.config.dataTypes.date\n        )\n\n        let resolver = ContentResolver(\n            contentTypeResolver: .init(\n                types: buildTargetSource.types,\n                pipelines: buildTargetSource.pipelines\n            ),\n            encoder: encoder,\n            decoder: decoder,\n            dateFormatter: dateFormatter\n        )\n\n        let targetContents = try resolver.convert(\n            rawContents: buildTargetSource.rawContents\n        )\n        #expect(!targetContents.isEmpty)\n        #expect(buildTargetSource.rawContents.count == targetContents.count)\n        for rawContent in buildTargetSource.rawContents {\n            guard\n                let item = targetContents.first(\n                    where: { $0.rawValue == rawContent }\n                )\n            else {\n                Issue.record(\"Missing content `\\(rawContent.origin.slug)`.\")\n                return\n            }\n            #expect(!item.isIterator)\n\n            let notFoundPages = [\"404\"]\n            let specialPages = [\"\", \"about\", \"context\"]\n            let redirectPages = [\"home-old\", \"about-old\"]\n            // check type identifiers\n            if !(specialPages + redirectPages + notFoundPages)\n                .contains(item.rawValue.origin.slug)\n            {\n                #expect(item.rawValue.origin.slug.contains(item.type.id))\n            }\n            else {\n                if specialPages.contains(item.rawValue.origin.slug) {\n                    #expect(item.type.id == \"page\")\n                }\n                else if notFoundPages.contains(item.rawValue.origin.slug) {\n                    #expect(item.type.id == \"not-found\")\n                }\n                else {\n                    #expect(item.type.id == \"redirect\")\n                }\n            }\n        }\n    }\n\n    // MARK: - content types\n\n    @Test\n    func defaultContentType() throws {\n        let now = Date()\n        let buildTargetSource = BuildTargetSource(\n            locations: .init(\n                sourceURL: .init(filePath: \"\"),\n                config: .defaults\n            ),\n            types: [\n                .init(\n                    id: \"page\",\n                    default: true\n                ),\n                .init(\n                    id: \"post\"\n                ),\n            ],\n            rawContents: [\n                .init(\n                    origin: .init(\n                        path: .init(\"hello\"),\n                        slug: \"hello\"\n                    ),\n                    lastModificationDate: now.timeIntervalSince1970,\n                    assetsPath: \"assets\",\n                    assets: []\n                )\n            ]\n        )\n\n        let validator = BuildTargetSourceValidator(\n            buildTargetSource: buildTargetSource\n        )\n        try validator.validate()\n\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n        let resolver = ContentResolver(\n            contentTypeResolver: .init(\n                types: buildTargetSource.types,\n                pipelines: buildTargetSource.pipelines\n            ),\n            encoder: encoder,\n            decoder: decoder,\n            dateFormatter: .init(\n                dateConfig: buildTargetSource.config.dataTypes.date\n            )\n        )\n\n        let targetContents = try resolver.convert(\n            rawContents: buildTargetSource.rawContents\n        )\n        #expect(targetContents.count == 1)\n        let content = try #require(targetContents.first)\n        #expect(content.type.id == \"page\")\n    }\n\n    @Test\n    func explicitContentType() throws {\n        let now = Date()\n        let buildTargetSource = BuildTargetSource(\n            locations: .init(\n                sourceURL: .init(filePath: \"\"),\n                config: .defaults\n            ),\n            types: [\n                .init(\n                    id: \"page\",\n                    default: true\n                ),\n                .init(\n                    id: \"post\",\n                    paths: [\n                        \"posts\"\n                    ]\n                ),\n            ],\n            rawContents: [\n                .init(\n                    origin: .init(\n                        path: .init(\"posts/hello\"),\n                        slug: \"posts/hello\"\n                    ),\n                    markdown: .init(\n                        frontMatter: [\n                            \"type\": \"post\"\n                        ]\n                    ),\n                    lastModificationDate: now.timeIntervalSince1970,\n                    assetsPath: \"assets\",\n                    assets: []\n                )\n            ]\n        )\n\n        let validator = BuildTargetSourceValidator(\n            buildTargetSource: buildTargetSource\n        )\n        try validator.validate()\n\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n        let resolver = ContentResolver(\n            contentTypeResolver: .init(\n                types: buildTargetSource.types,\n                pipelines: buildTargetSource.pipelines\n            ),\n            encoder: encoder,\n            decoder: decoder,\n            dateFormatter: .init(\n                dateConfig: buildTargetSource.config.dataTypes.date\n            )\n        )\n\n        let targetContents = try resolver.convert(\n            rawContents: buildTargetSource.rawContents\n        )\n        #expect(targetContents.count == 1)\n        let content = try #require(targetContents.first)\n        #expect(content.type.id == \"post\")\n    }\n\n    @Test\n    func pathBasedContentType() throws {\n        let now = Date()\n        let buildTargetSource = BuildTargetSource(\n            locations: .init(\n                sourceURL: .init(filePath: \"\"),\n                config: .defaults\n            ),\n            types: [\n                .init(\n                    id: \"page\",\n                    default: true\n                ),\n                .init(\n                    id: \"post\",\n                    paths: [\n                        \"posts\"\n                    ]\n                ),\n            ],\n            rawContents: [\n                .init(\n                    origin: .init(\n                        path: .init(\"posts/hello\"),\n                        slug: \"posts/hello\"\n                    ),\n                    lastModificationDate: now.timeIntervalSince1970,\n                    assetsPath: \"assets\",\n                    assets: []\n                )\n            ]\n        )\n\n        let validator = BuildTargetSourceValidator(\n            buildTargetSource: buildTargetSource\n        )\n        try validator.validate()\n\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n        let resolver = ContentResolver(\n            contentTypeResolver: .init(\n                types: buildTargetSource.types,\n                pipelines: buildTargetSource.pipelines\n            ),\n            encoder: encoder,\n            decoder: decoder,\n            dateFormatter: .init(\n                dateConfig: buildTargetSource.config.dataTypes.date\n            )\n        )\n\n        let targetContents = try resolver.convert(\n            rawContents: buildTargetSource.rawContents\n        )\n        #expect(targetContents.count == 1)\n        let content = try #require(targetContents.first)\n        #expect(content.type.id == \"post\")\n    }\n\n    @Test()\n    func missingContentType() async throws {\n        let now = Date()\n        let buildTargetSource = BuildTargetSource(\n            locations: .init(\n                sourceURL: .init(filePath: \"\"),\n                config: .defaults\n            ),\n            types: [\n                .init(\n                    id: \"page\",\n                    default: true\n                )\n            ],\n            rawContents: [\n                .init(\n                    origin: .init(\n                        path: .init(\"foo/bar\"),\n                        slug: \"foo/bar\"\n                    ),\n                    markdown: .init(\n                        frontMatter: [\n                            \"type\": \"foo\"\n                        ]\n                    ),\n                    lastModificationDate: now.timeIntervalSince1970,\n                    assetsPath: \"assets\",\n                    assets: []\n                )\n            ]\n        )\n\n        let validator = BuildTargetSourceValidator(\n            buildTargetSource: buildTargetSource\n        )\n        try validator.validate()\n\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n        let resolver = ContentResolver(\n            contentTypeResolver: .init(\n                types: buildTargetSource.types,\n                pipelines: buildTargetSource.pipelines\n            ),\n            encoder: encoder,\n            decoder: decoder,\n            dateFormatter: .init(\n                dateConfig: buildTargetSource.config.dataTypes.date\n            )\n        )\n\n        do {\n            _ = try resolver.convert(\n                rawContents: buildTargetSource.rawContents\n            )\n            Issue.record(\"Should result in an missing content type error.\")\n        }\n        catch {\n            switch error {\n            case let .contentType(typeError):\n                switch typeError {\n                case let .missingContentType(id, path):\n                    #expect(id == \"foo\")\n                    #expect(path == \"foo/bar\")\n                default:\n                    Issue.record(\"Invalid content type error result.\")\n                }\n            default:\n                Issue.record(\"Invalid content resolver error result.\")\n            }\n        }\n    }\n\n    @Test()\n    func allPropertyTypeConversion() async throws {\n        let now = Date()\n        let buildTargetSource = BuildTargetSource(\n            locations: .init(\n                sourceURL: .init(filePath: \"\"),\n                config: .defaults\n            ),\n            types: [\n                .init(\n                    id: \"test\",\n                    default: true,\n                    properties: [\n                        \"string\": .init(\n                            propertyType: .string,\n                            isRequired: true,\n                            defaultValue: nil\n                        ),\n                        \"bool\": .init(\n                            propertyType: .bool,\n                            isRequired: true,\n                            defaultValue: .init(2)\n                        ),\n                        \"int\": .init(\n                            propertyType: .int,\n                            isRequired: true,\n                            defaultValue: .init(2)\n                        ),\n                        \"double\": .init(\n                            propertyType: .double,\n                            isRequired: true,\n                            defaultValue: nil\n                        ),\n                        \"date\": .init(\n                            propertyType: .date(\n                                config: nil\n                            ),\n                            isRequired: true,\n                            defaultValue: nil\n                        ),\n                        \"array\": .init(\n                            propertyType: .array(\n                                of: .string\n                            ),\n                            isRequired: true,\n                            defaultValue: nil\n                        ),\n                    ]\n                )\n            ],\n            rawContents: [\n                .init(\n                    origin: .init(\n                        path: .init(\"test\"),\n                        slug: \"test\"\n                    ),\n                    markdown: .init(\n                        frontMatter: [\n                            \"string\": .init(\"foo\"),\n                            \"bool\": .init(true),\n                            \"int\": .init(42),\n                            \"double\": .init(3.14),\n                            \"date\": .init(\"2025-03-30T09:23:14.870Z\"),\n                            \"array\": .init([\"1\", \"2\"]),\n                        ]\n                    ),\n                    lastModificationDate: now.timeIntervalSince1970,\n                    assetsPath: \"assets\",\n                    assets: []\n                )\n            ]\n        )\n\n        let validator = BuildTargetSourceValidator(\n            buildTargetSource: buildTargetSource\n        )\n        try validator.validate()\n\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n        let resolver = ContentResolver(\n            contentTypeResolver: .init(\n                types: buildTargetSource.types,\n                pipelines: buildTargetSource.pipelines\n            ),\n            encoder: encoder,\n            decoder: decoder,\n            dateFormatter: .init(\n                dateConfig: buildTargetSource.config.dataTypes.date\n            )\n        )\n\n        let targetContents = try resolver.convert(\n            rawContents: buildTargetSource.rawContents\n        )\n        #expect(targetContents.count == 1)\n        let result = try #require(targetContents.first).properties\n        #expect(result.count == 6)\n        #expect(result[\"string\"] == \"foo\")\n        #expect(result[\"bool\"] == true)\n        #expect(result[\"int\"] == 42)\n        #expect(result[\"double\"] == 3.14)\n        #expect(result[\"date\"] == 1_743_326_594.87)\n        #expect(result[\"array\"]?.value(as: [String].self) == [\"1\", \"2\"])\n    }\n\n    @Test()\n    func contentDatePropertyConversion() async throws {\n        let now = Date()\n        let buildTargetSource = BuildTargetSource(\n            locations: .init(\n                sourceURL: .init(filePath: \"\"),\n                config: .defaults\n            ),\n            types: [\n                .init(\n                    id: \"definition\",\n                    default: true,\n                    properties: [\n                        \"defaultFormat\": .init(\n                            propertyType: .date(\n                                config: nil\n                            ),\n                            isRequired: true\n                        ),\n                        \"customFormat\": .init(\n                            propertyType: .date(\n                                config: .init(\n                                    localization: .defaults,\n                                    format: \"y-MM-d\"\n                                )\n                            ),\n                            isRequired: true\n                        ),\n                        \"customFormatDefaultValue\": .init(\n                            propertyType: .date(\n                                config: .init(\n                                    localization: .defaults,\n                                    format: \"y-MM-d\"\n                                )\n                            ),\n                            isRequired: true,\n                            defaultValue: .init(\"2021-03-03\")\n                        ),\n                    ]\n                )\n            ],\n            rawContents: [\n                .init(\n                    origin: .init(\n                        path: .init(\"test\"),\n                        slug: \"test\"\n                    ),\n                    markdown: .init(\n                        frontMatter: [\n                            \"defaultFormat\": \"2025-03-30T09:23:14.870Z\",\n                            \"customFormat\": \"2021-03-05\",\n                        ]\n                    ),\n                    lastModificationDate: now.timeIntervalSince1970,\n                    assetsPath: \"assets\",\n                    assets: []\n                )\n            ]\n        )\n\n        let validator = BuildTargetSourceValidator(\n            buildTargetSource: buildTargetSource\n        )\n        try validator.validate()\n\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n        let resolver = ContentResolver(\n            contentTypeResolver: .init(\n                types: buildTargetSource.types,\n                pipelines: buildTargetSource.pipelines\n            ),\n            encoder: encoder,\n            decoder: decoder,\n            dateFormatter: .init(\n                dateConfig: buildTargetSource.config.dataTypes.date\n            )\n        )\n\n        let targetContents = try resolver.convert(\n            rawContents: buildTargetSource.rawContents\n        )\n        #expect(targetContents.count == 1)\n        let result = try #require(targetContents.first).properties\n\n        #expect(\n            result[\"customFormat\"] == .init(1_614_902_400.0)\n        )\n        #expect(\n            result[\"customFormatDefaultValue\"] == .init(1_614_729_600.0)\n        )\n        #expect(\n            result[\"defaultFormat\"] == .init(1_743_326_594.87)\n        )\n    }\n\n    @Test()\n    func contentDatePropertyConversionInvalidValue() async throws {\n        let now = Date()\n        let buildTargetSource = BuildTargetSource(\n            locations: .init(\n                sourceURL: .init(filePath: \"\"),\n                config: .defaults\n            ),\n            types: [\n                .init(\n                    id: \"test\",\n                    default: true,\n                    properties: [\n                        \"monthAndDay\": .init(\n                            propertyType: .date(\n                                config: .init(\n                                    localization: .defaults,\n                                    format: \"MM-d\"\n                                )\n                            ),\n                            isRequired: true\n                        )\n                    ]\n                )\n            ],\n            rawContents: [\n                .init(\n                    origin: .init(\n                        path: .init(\"test\"),\n                        slug: \"test\"\n                    ),\n                    markdown: .init(\n                        frontMatter: [\n                            \"monthAndDay\": .init(\"2021-03-05\")\n                        ]\n                    ),\n                    lastModificationDate: now.timeIntervalSince1970,\n                    assetsPath: \"assets\",\n                    assets: []\n                )\n            ]\n        )\n\n        let validator = BuildTargetSourceValidator(\n            buildTargetSource: buildTargetSource\n        )\n        try validator.validate()\n\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n        let resolver = ContentResolver(\n            contentTypeResolver: .init(\n                types: buildTargetSource.types,\n                pipelines: buildTargetSource.pipelines\n            ),\n            encoder: encoder,\n            decoder: decoder,\n            dateFormatter: .init(\n                dateConfig: buildTargetSource.config.dataTypes.date\n            )\n        )\n\n        do {\n            _ = try resolver.convert(\n                rawContents: buildTargetSource.rawContents\n            )\n            Issue.record(\"Should result in an invalid property error.\")\n        }\n        catch {\n            switch error {\n            case let .invalidProperty(name, value, slug):\n                #expect(name == \"monthAndDay\")\n                #expect(value == \"2021-03-05\")\n                #expect(slug == \"test\")\n            default:\n                Issue.record(\"Invalid error result.\")\n            }\n        }\n    }\n\n    @Test()\n    func contentDatePropertyConversionInvalidValueWithDefaultValue()\n        async throws\n    {\n        let now = Date()\n        let buildTargetSource = BuildTargetSource(\n            locations: .init(\n                sourceURL: .init(filePath: \"\"),\n                config: .defaults\n            ),\n            types: [\n                .init(\n                    id: \"test\",\n                    default: true,\n                    properties: [\n                        \"monthAndDay\": .init(\n                            propertyType: .date(\n                                config: .init(\n                                    localization: .defaults,\n                                    format: \"MM-d\"\n                                )\n                            ),\n                            isRequired: true,\n                            defaultValue: .init(\"03-30\")\n                        )\n                    ]\n                )\n            ],\n            rawContents: [\n                .init(\n                    origin: .init(\n                        path: .init(\"test\"),\n                        slug: \"test\"\n                    ),\n                    markdown: .init(\n                        frontMatter: [\n                            \"monthAndDay\": .init(\"2021-03-05\")\n                        ]\n                    ),\n                    lastModificationDate: now.timeIntervalSince1970,\n                    assetsPath: \"assets\",\n                    assets: []\n                )\n            ]\n        )\n\n        let validator = BuildTargetSourceValidator(\n            buildTargetSource: buildTargetSource\n        )\n        try validator.validate()\n\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n        let resolver = ContentResolver(\n            contentTypeResolver: .init(\n                types: buildTargetSource.types,\n                pipelines: buildTargetSource.pipelines\n            ),\n            encoder: encoder,\n            decoder: decoder,\n            dateFormatter: .init(\n                dateConfig: buildTargetSource.config.dataTypes.date\n            )\n        )\n\n        do {\n            _ = try resolver.convert(\n                rawContents: buildTargetSource.rawContents\n            )\n            Issue.record(\"Should result in an invalid property error.\")\n        }\n        catch {\n            switch error {\n            case let .invalidProperty(name, value, slug):\n                #expect(name == \"monthAndDay\")\n                #expect(value == \"2021-03-05\")\n                #expect(slug == \"test\")\n            default:\n                Issue.record(\"Invalid error result.\")\n            }\n        }\n    }\n\n    @Test()\n    func genericFilterRules() async throws {\n        let now = Date()\n        let buildTargetSource = Mocks.buildTargetSource(now: now)\n        let resolver = try getMockresolver(\n            buildTargetSource: buildTargetSource,\n            now: now\n        )\n        let contents = try resolver.convert(\n            rawContents: buildTargetSource.rawContents\n        )\n\n        let result = resolver.apply(\n            filterRules: [\n                \"*\": .or(\n                    [\n                        .field(\n                            key: \"title\",\n                            operator: .like,\n                            value: \"1\"\n                        ),\n                        .field(\n                            key: \"name\",\n                            operator: .like,\n                            value: \"1\"\n                        ),\n                    ]\n                )\n            ],\n            to: contents,\n            now: now.timeIntervalSince1970\n        )\n\n        let expGroups = Dictionary(\n            grouping: contents,\n            by: { $0.type.id }\n        )\n\n        let resGroups = Dictionary(\n            grouping: result,\n            by: { $0.type.id }\n        )\n\n        #expect(result.count < contents.count)\n\n        for key in expGroups.keys {\n            let exp1 =\n                expGroups[key]?\n                .filter {\n                    $0.properties[\"title\"]?.stringValue()?.hasSuffix(\"1\")\n                        ?? $0.properties[\"name\"]?.stringValue()?.hasSuffix(\"1\")\n                        ?? false\n                } ?? []\n\n            let res1 =\n                resGroups[key]?\n                .filter {\n                    $0.properties[\"title\"]?.stringValue()?.hasSuffix(\"1\")\n                        ?? $0.properties[\"name\"]?.stringValue()?\n                        .hasSuffix(\"1\")\n                        ?? false\n                } ?? []\n\n            #expect(res1.count == exp1.count)\n            for i in 0..<res1.count {\n                #expect(res1[i].slug == exp1[i].slug)\n            }\n        }\n    }\n\n    @Test()\n    func specificFilterRules() async throws {\n        let now = Date()\n        let buildTargetSource = Mocks.buildTargetSource(now: now)\n        let resolver = try getMockresolver(\n            buildTargetSource: buildTargetSource,\n            now: now\n        )\n        let contents = try resolver.convert(\n            rawContents: buildTargetSource.rawContents\n        )\n\n        let result = resolver.apply(\n            filterRules: [\n                \"*\": .or(\n                    [\n                        .field(\n                            key: \"title\",\n                            operator: .like,\n                            value: \"10\"\n                        ),\n                        .field(\n                            key: \"name\",\n                            operator: .like,\n                            value: \"10\"\n                        ),\n                    ]\n                ),\n                \"post\": .field(\n                    key: \"featured\",\n                    operator: .equals,\n                    value: true\n                ),\n            ],\n            to: contents,\n            now: now.timeIntervalSince1970\n        )\n\n        #expect(result.count < contents.count)\n\n        let expGroups = Dictionary(\n            grouping: contents,\n            by: { $0.type.id }\n        )\n\n        let resGroups = Dictionary(\n            grouping: result,\n            by: { $0.type.id }\n        )\n\n        for key in expGroups.keys {\n            let exp1 =\n                expGroups[key]?\n                .filter {\n                    if key == \"post\" {\n                        return $0.properties[\"featured\"]?\n                            .boolValue() ?? false\n                    }\n                    return $0.properties[\"title\"]?.stringValue()?\n                        .hasSuffix(\"10\") ?? $0.properties[\"name\"]?\n                        .stringValue()?\n                        .hasSuffix(\"10\") ?? false\n                } ?? []\n\n            let res1 =\n                resGroups[key]?\n                .filter {\n                    if key == \"post\" {\n                        return $0.properties[\"featured\"]?\n                            .boolValue() ?? false\n                    }\n                    return $0.properties[\"title\"]?.stringValue()?\n                        .hasSuffix(\"10\") ?? $0.properties[\"name\"]?\n                        .stringValue()?\n                        .hasSuffix(\"10\") ?? false\n                } ?? []\n\n            #expect(res1.count == exp1.count)\n            for i in 0..<res1.count {\n                #expect(res1[i].slug == exp1[i].slug)\n            }\n        }\n    }\n\n    @Test()\n    func noFilterRules() async throws {\n        let now = Date()\n        let buildTargetSource = Mocks.buildTargetSource(now: now)\n        let resolver = try getMockresolver(\n            buildTargetSource: buildTargetSource,\n            now: now\n        )\n        let contents = try resolver.convert(\n            rawContents: buildTargetSource.rawContents\n        )\n\n        let result = resolver.apply(\n            filterRules: [:],\n            to: contents,\n            now: now.timeIntervalSince1970\n        )\n\n        #expect(result.count == contents.count)\n\n        let expGroups = Dictionary(\n            grouping: contents,\n            by: { $0.type.id }\n        )\n\n        let resGroups = Dictionary(\n            grouping: result,\n            by: { $0.type.id }\n        )\n\n        for key in expGroups.keys {\n            #expect(expGroups[key]?.count == resGroups[key]?.count)\n        }\n    }\n\n    @Test()\n    func globalDateFilter() async throws {\n        let now = Date()\n        let future = now.addingTimeInterval(+86400)\n        let past = now.addingTimeInterval(-86400)\n\n        let config = Config.defaults\n        let dateFormatter = ToucanInputDateFormatter(\n            dateConfig: config.dataTypes.date\n        )\n\n        // make sure we use the same format\n        let format: DateFormatterConfig? = config.dataTypes.date.input\n\n        let buildTargetSource = BuildTargetSource(\n            locations: .init(\n                sourceURL: .init(filePath: \"\"),\n                config: .defaults\n            ),\n            config: config,\n            types: [\n                .init(\n                    id: \"post\",\n                    default: true,\n                    properties: [\n                        \"publication\": .init(\n                            propertyType: .date(config: format),\n                            isRequired: true\n                        ),\n                        \"expiration\": .init(\n                            propertyType: .date(config: format),\n                            isRequired: true\n                        ),\n                    ]\n                )\n            ],\n            rawContents: [\n                .init(\n                    origin: .init(\n                        path: .init(\"test1\"),\n                        slug: \"test1\"\n                    ),\n                    markdown: .init(\n                        frontMatter: [\n                            \"publication\": .init(\n                                // NOTE: not the best way, but it's ok for tests\n                                dateFormatter.string(\n                                    from: past,\n                                    using: format\n                                )\n                            ),\n                            \"expiration\": .init(\n                                dateFormatter.string(\n                                    from: future,\n                                    using: format\n                                )\n                            ),\n                        ]\n                    ),\n                    lastModificationDate: now.timeIntervalSince1970,\n                    assetsPath: \"assets\",\n                    assets: []\n                ),\n                .init(\n                    origin: .init(\n                        path: .init(\"test2\"),\n                        slug: \"test2\"\n                    ),\n                    markdown: .init(\n                        frontMatter: [\n                            \"publication\": .init(\n                                dateFormatter.string(\n                                    from: future,\n                                    using: format\n                                )\n                            ),\n                            \"expiration\": .init(\n                                dateFormatter.string(\n                                    from: future,\n                                    using: format\n                                )\n                            ),\n                        ]\n                    ),\n                    lastModificationDate: now.timeIntervalSince1970,\n                    assetsPath: \"assets\",\n                    assets: []\n                ),\n            ]\n        )\n\n        let validator = BuildTargetSourceValidator(\n            buildTargetSource: buildTargetSource\n        )\n        try validator.validate()\n\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n        let resolver = ContentResolver(\n            contentTypeResolver: .init(\n                types: buildTargetSource.types,\n                pipelines: buildTargetSource.pipelines\n            ),\n            encoder: encoder,\n            decoder: decoder,\n            dateFormatter: dateFormatter\n        )\n\n        let contents = try resolver.convert(\n            rawContents: buildTargetSource.rawContents\n        )\n\n        let result = resolver.apply(\n            filterRules: [\n                \"post\": .and(\n                    [\n                        .field(\n                            key: \"publication\",\n                            operator: .lessThan,\n                            value: \"{{date.now}}\"\n                        ),\n                        .field(\n                            key: \"expiration\",\n                            operator: .greaterThan,\n                            value: \"{{date.now}}\"\n                        ),\n                    ]\n                )\n            ],\n            to: contents,\n            now: now.timeIntervalSince1970\n        )\n        #expect(result.count == 1)\n        #expect(result[0].slug.value == \"test1\")\n    }\n\n    @Test()\n    func draftFilter() async throws {\n        let now = Date()\n\n        let buildTargetSource = BuildTargetSource(\n            locations: .init(\n                sourceURL: .init(filePath: \"\"),\n                config: .defaults\n            ),\n            types: [\n                .init(\n                    id: \"post\",\n                    default: true,\n                    properties: [\n                        \"draft\": .init(\n                            propertyType: .bool,\n                            isRequired: false,\n                            defaultValue: false\n                        )\n                    ]\n                )\n            ],\n            rawContents: [\n                .init(\n                    origin: .init(\n                        path: .init(\"test1\"),\n                        slug: \"test1\"\n                    ),\n                    markdown: .init(\n                        frontMatter: [\n                            \"draft\": false\n                        ]\n                    ),\n                    lastModificationDate: now.timeIntervalSince1970,\n                    assetsPath: \"assets\",\n                    assets: []\n                ),\n                .init(\n                    origin: .init(\n                        path: .init(\"test2\"),\n                        slug: \"test2\"\n                    ),\n                    markdown: .init(\n                        frontMatter: [\n                            \"draft\": true\n                        ]\n                    ),\n                    lastModificationDate: now.timeIntervalSince1970,\n                    assetsPath: \"assets\",\n                    assets: []\n                ),\n            ]\n        )\n\n        let validator = BuildTargetSourceValidator(\n            buildTargetSource: buildTargetSource\n        )\n        try validator.validate()\n\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n        let resolver = ContentResolver(\n            contentTypeResolver: .init(\n                types: buildTargetSource.types,\n                pipelines: buildTargetSource.pipelines\n            ),\n            encoder: encoder,\n            decoder: decoder,\n            dateFormatter: .init(\n                dateConfig: buildTargetSource.config.dataTypes.date\n            )\n        )\n\n        let contents = try resolver.convert(\n            rawContents: buildTargetSource.rawContents\n        )\n\n        let result = resolver.apply(\n            filterRules: [\n                \"*\": .field(\n                    key: \"draft\",\n                    operator: .equals,\n                    value: false\n                )\n            ],\n            to: contents,\n            now: now.timeIntervalSince1970\n        )\n        #expect(result.count == 1)\n        #expect(result[0].slug.value == \"test1\")\n    }\n\n    // MARK: - iterators\n\n    @Test\n    func iteratorResolution() async throws {\n        let now = Date()\n        let buildTargetSource = Mocks.buildTargetSource(now: now)\n        let resolver = try getMockresolver(\n            buildTargetSource: buildTargetSource,\n            now: now\n        )\n        let baseContents = try resolver.convert(\n            rawContents: buildTargetSource.rawContents\n        )\n        let pipeline = Mocks.Pipelines.html()\n\n        let contents = resolver.apply(\n            iterators: pipeline.iterators,\n            to: baseContents,\n            baseURL: buildTargetSource.target.url.dropTrailingSlash(),\n            now: now.timeIntervalSince1970\n        )\n\n        let query = Query(\n            contentType: \"page\",\n            filter: .field(\n                key: RootContextKeys.iterator.rawValue,\n                operator: .equals,\n                value: true\n            )\n        )\n\n        let results = contents.run(\n            query: query,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentResolverTestSuite\")\n        )\n\n        try #require(results.count == 2)\n        #expect(\n            results.map(\\.slug.value).sorted() == [\n                \"blog/posts/pages/1\",\n                \"blog/posts/pages/2\",\n            ]\n        )\n    }\n\n    // MARK: - asset resolver\n\n    @Test\n    func assetBehaviorBasics() async throws {\n        let now = Date()\n        let buildTargetSource = Mocks.buildTargetSource(now: now)\n        let resolver = try getMockresolver(\n            buildTargetSource: buildTargetSource,\n            now: now\n        )\n        let baseContents = try resolver.convert(\n            rawContents: buildTargetSource.rawContents\n        )\n        let pipeline = Mocks.Pipelines.html()\n\n        let contents = try resolver.apply(\n            assetProperties: pipeline.assets.properties,\n            to: baseContents,\n            contentsURL: buildTargetSource.locations.contentsURL,\n            assetsPath: buildTargetSource.config.contents.assets.path,\n            baseURL: buildTargetSource.target.url.dropTrailingSlash()\n        )\n\n        let query1 = Query(\n            contentType: \"post\"\n        )\n\n        let results1 = contents.run(\n            query: query1,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentResolverTestSuite\")\n        )\n\n        try #require(results1.count == 3)\n\n        let images = results1.compactMap {\n            $0.properties[\"image\"]?.stringValue()\n        }\n\n        #expect(\n            images.sorted() == [\n                \"http://localhost:3000/assets/blog/posts/post-1/cover.jpg\",\n                \"http://localhost:3000/assets/blog/posts/post-2/cover.jpg\",\n                \"http://localhost:3000/assets/blog/posts/post-3/cover.jpg\",\n            ]\n        )\n\n        let query2 = Query(\n            contentType: \"page\",\n            filter: .field(\n                key: \"slug\",\n                operator: .equals,\n                value: \"about\"\n            )\n        )\n\n        let results2 = contents.run(\n            query: query2,\n            now: now.timeIntervalSince1970,\n            logger: .init(label: \"ContentResolverTestSuite\")\n        )\n\n        try #require(results2.count == 1)\n\n        let css =\n            results2[0].properties[\"css\"]?.arrayValue(as: String.self) ?? []\n        let js = results2[0].properties[\"js\"]?.arrayValue(as: String.self) ?? []\n        #expect(\n            css.sorted() == [\n                // @NOTE: maybe support resolving ./assets/file.css ???\n                \"/assets/about/about.css\",\n                \"http://localhost:3000/assets/about/style.css\",\n                \"https://unpkg.com/test@1.0.0.css\",\n            ]\n        )\n        #expect(\n            js.sorted() == [\n                \"main.js\"\n            ]\n        )\n    }\n\n    @Test()\n    func contentFrontMatterSlugInvalidValue() async throws {\n        let now = Date()\n        let buildTargetSource = BuildTargetSource(\n            locations: .init(\n                sourceURL: .init(filePath: \"\"),\n                config: .defaults\n            ),\n            types: [\n                .init(\n                    id: \"test\",\n                    default: true\n                )\n            ],\n            rawContents: [\n                .init(\n                    origin: .init(\n                        path: .init(\"test\"),\n                        slug: \"test\"\n                    ),\n                    markdown: .init(\n                        frontMatter: [\n                            \"slug\": \"te st\"\n                        ]\n                    ),\n                    lastModificationDate: now.timeIntervalSince1970,\n                    assetsPath: \"assets\",\n                    assets: []\n                )\n            ]\n        )\n\n        let validator = BuildTargetSourceValidator(\n            buildTargetSource: buildTargetSource\n        )\n        try validator.validate()\n\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n        let resolver = ContentResolver(\n            contentTypeResolver: .init(\n                types: buildTargetSource.types,\n                pipelines: buildTargetSource.pipelines\n            ),\n            encoder: encoder,\n            decoder: decoder,\n            dateFormatter: .init(\n                dateConfig: buildTargetSource.config.dataTypes.date\n            )\n        )\n\n        do {\n            _ = try resolver.convert(\n                rawContents: buildTargetSource.rawContents\n            )\n            Issue.record(\"Should result in an invalid slug error.\")\n        }\n        catch {\n            switch error {\n            case let .invalidSlug(slug):\n                #expect(slug == \"te st\")\n            default:\n                Issue.record(\"Invalid error result.\")\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSDKTests/DateFormatter/ToucanDateFormatterTestSuite.swift",
    "content": "//\n//  ToucanDateFormatterTestSuite.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 04. 17..\n//\n\nimport Foundation\nimport Logging\nimport Testing\n@testable import ToucanSDK\nimport ToucanSource\n\n@Suite\nstruct ToucanDateFormatterTestSuite {\n    @Test\n    func input() throws {\n        let config = Config.defaults\n\n        let dateFormatter = ToucanInputDateFormatter(\n            dateConfig: config.dataTypes.date\n        )\n\n        let dateString = \"2001-01-01T00:00:00.000Z\"\n\n        let inputDate = try #require(\n            dateFormatter.date(from: dateString)\n        )\n        #expect(inputDate.timeIntervalSinceReferenceDate == 0)\n\n        let localizedInputDate = try #require(\n            dateFormatter.date(\n                from: dateString,\n                using: .init(\n                    localization: .init(\n                        locale: \"hu-HU\",\n                        timeZone: \"CET\"\n                    ),\n                    format: \"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\"\n                )\n            )\n        )\n        #expect(localizedInputDate.timeIntervalSinceReferenceDate == -3600)\n    }\n\n    @Test\n    func output() throws {\n        let config = Config.defaults\n        var pipeline = Mocks.Pipelines.html()\n        pipeline.dataTypes.date.output = .init(\n            locale: \"hu-HU\",\n            timeZone: \"CET\"\n        )\n\n        let dateFormatter = ToucanOutputDateFormatter(\n            dateConfig: config.dataTypes.date,\n            pipelineDateConfig: pipeline.dataTypes.date\n        )\n\n        let date = Date(timeIntervalSinceReferenceDate: 0)\n        let ctx = dateFormatter.format(date)\n\n        #expect(ctx.date.full == \"2001. január 1., hétfő\")\n        #expect(ctx.date.long == \"2001. január 1.\")\n        #expect(ctx.date.medium == \"2001. jan. 1.\")\n        #expect(ctx.date.short == \"2001. 01. 01.\")\n\n        #expect(ctx.time.full == \"1:00:00 közép-európai téli idő\")\n        #expect(ctx.time.long == \"1:00:00 CET\")\n        #expect(ctx.time.medium == \"1:00:00\")\n        #expect(ctx.time.short == \"1:00\")\n\n        #expect(ctx.timestamp == 978_307_200)\n        #expect(ctx.iso8601 == \"2001-01-01T01:00:00.000Z\")\n\n        #expect(ctx.formats[\"rss\"] == \"Mon, 01 Jan 2001 00:00:00 +0000\")\n        #expect(ctx.formats[\"year\"] == \"2001\")\n        #expect(ctx.formats[\"sitemap\"] == \"2001-01-01\")\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSDKTests/E2ETestSuite.swift",
    "content": "//\n//  E2ETestSuite.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 06. 11..\n//\n\nimport FileManagerKitBuilder\nimport Foundation\nimport Logging\nimport Testing\nimport ToucanCore\n@testable import ToucanSDK\nimport ToucanSource\n\n@Suite\nstruct E2ETestSuite {\n\n    @Test\n    func upperLevelDirectoryPathEncoding() throws {\n        let now = Date()\n\n        try FileManagerPlayground {\n            Directory(name: \"Hello World\") {\n                Mocks.E2E.src(now: now)\n            }\n        }\n        .test {\n            let upperDir = $1.appendingPathIfPresent(\"Hello World\")\n            let workDir = upperDir.appendingPathIfPresent(\"src\")\n\n            let toucan = Toucan()\n            // NOTE: no need to percent encode the input dir in this case\n            let inputDir = workDir.path(percentEncoded: false)\n            try toucan.generate(\n                workDir: inputDir,\n                now: now\n            )\n\n            let distURL = workDir.appendingPathIfPresent(\"dist\")\n            let notFoundURL = distURL.appendingPathIfPresent(\"404.html\")\n            let notFound = try String(contentsOf: notFoundURL, encoding: .utf8)\n\n            #expect(notFound.contains(\"Not found page contents\"))\n        }\n    }\n\n    @Test\n    func notFound() throws {\n        let now = Date()\n\n        try FileManagerPlayground {\n            Mocks.E2E.src(\n                now: now\n            )\n        }\n        .test {\n            let workDir = $1.appendingPathIfPresent(\"src\")\n            let toucan = Toucan()\n\n            try toucan.generate(\n                workDir: workDir.path(),\n                now: now\n            )\n\n            let distURL = workDir.appendingPathIfPresent(\"dist\")\n            let notFoundURL = distURL.appendingPathIfPresent(\"404.html\")\n            let notFound = try String(contentsOf: notFoundURL, encoding: .utf8)\n\n            #expect(notFound.contains(\"Not found page contents\"))\n        }\n    }\n\n    // MARK: - non-html files\n\n    @Test\n    func rss() throws {\n        let now = Date()\n\n        try FileManagerPlayground {\n            Mocks.E2E.src(now: now)\n        }\n        .test {\n            let workDir = $1.appendingPathIfPresent(\"src\")\n            let toucan = Toucan()\n            try toucan.generate(\n                workDir: workDir.path(),\n                now: now\n            )\n\n            let distURL = workDir.appendingPathIfPresent(\"dist\")\n            let rssXML = distURL.appendingPathIfPresent(\"rss.xml\")\n            let rss = try String(contentsOf: rssXML, encoding: .utf8)\n\n            let formatter = ToucanOutputDateFormatter(\n                dateConfig: Config.defaults.dataTypes.date,\n                pipelineDateConfig: Mocks.Pipelines.rss().dataTypes.date\n            )\n\n            let nowString = formatter.format(now).formats[\"rss\"] ?? \"\"\n            let post1date =\n                formatter.format(now.addingTimeInterval(-86400)).formats[\"rss\"]\n                ?? \"\"\n            let post2date =\n                formatter.format(now.addingTimeInterval(-86400 * 2))\n                .formats[\"rss\"] ?? \"\"\n            let post3date =\n                formatter.format(now.addingTimeInterval(-86400 * 3))\n                .formats[\"rss\"] ?? \"\"\n\n            let expectation = #\"\"\"\n                <rss xmlns:atom=\"http://www.w3.org/2005/Atom\" version=\"2.0\">\n                <channel>\n                    <title>Test site name</title>\n                    <description>Test site description</description>\n                    <link>http://localhost:3000</link>\n                    <language>en-US</language>\n                    <lastBuildDate>\\#(nowString)</lastBuildDate>\n                    <pubDate>\\#(nowString)</pubDate>\n                    <ttl>250</ttl>\n                    <atom:link href=\"http://localhost:3000/rss.xml\" rel=\"self\" type=\"application/rss+xml\"/>\n\n                    <item>\n                        <guid isPermaLink=\"true\">http://localhost:3000/blog/posts/post-1/</guid>\n                        <title><![CDATA[ Post #1 ]]></title>\n                        <description><![CDATA[ Post #1 description ]]></description>\n                        <link>http://localhost:3000/blog/posts/post-1/</link>\n                        <pubDate>\\#(post1date)</pubDate>\n                    </item>\n                    <item>\n                        <guid isPermaLink=\"true\">http://localhost:3000/blog/posts/post-2/</guid>\n                        <title><![CDATA[ Post #2 ]]></title>\n                        <description><![CDATA[ Post #2 description ]]></description>\n                        <link>http://localhost:3000/blog/posts/post-2/</link>\n                        <pubDate>\\#(post2date)</pubDate>\n                    </item>\n                    <item>\n                        <guid isPermaLink=\"true\">http://localhost:3000/blog/posts/post-3/</guid>\n                        <title><![CDATA[ Post #3 ]]></title>\n                        <description><![CDATA[ Post #3 description ]]></description>\n                        <link>http://localhost:3000/blog/posts/post-3/</link>\n                        <pubDate>\\#(post3date)</pubDate>\n                    </item>\n\n                </channel>\n                </rss>\n                \"\"\"#\n\n            #expect(\n                rss.trimmingCharacters(in: .whitespacesAndNewlines)\n                    == expectation.trimmingCharacters(\n                        in: .whitespacesAndNewlines\n                    )\n            )\n        }\n    }\n\n    @Test\n    func sitemap() throws {\n        let now = Date()\n\n        try FileManagerPlayground {\n            Mocks.E2E.src(now: now)\n        }\n        .test {\n\n            let workDir = $1.appendingPathIfPresent(\"src\")\n\n            let toucan = Toucan()\n            try toucan.generate(\n                workDir: workDir.path(),\n                now: now\n            )\n\n            let distURL = workDir.appendingPathIfPresent(\"dist\")\n            let sitemapXML = distURL.appendingPathIfPresent(\"sitemap.xml\")\n            let sitemap = try String(contentsOf: sitemapXML, encoding: .utf8)\n\n            let formatter = ToucanOutputDateFormatter(\n                dateConfig: Config.defaults.dataTypes.date,\n                pipelineDateConfig: Mocks.Pipelines.sitemap().dataTypes.date\n            )\n\n            let nowString = formatter.format(now).formats[\"sitemap\"] ?? \"\"\n\n            let expectation = #\"\"\"\n                <urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n                    <url>\n\n                        <loc>http://localhost:3000/blog/posts/pages/1/</loc>\n                        <lastmod>\\#(nowString)</lastmod>\n                        <loc>http://localhost:3000/blog/posts/pages/2/</loc>\n                        <lastmod>\\#(nowString)</lastmod>\n                        <loc>http://localhost:3000/pages/page-3/</loc>\n                        <lastmod>\\#(nowString)</lastmod>\n                        <loc>http://localhost:3000/pages/page-2/</loc>\n                        <lastmod>\\#(nowString)</lastmod>\n                        <loc>http://localhost:3000/pages/page-1/</loc>\n                        <lastmod>\\#(nowString)</lastmod>\n                        <loc>http://localhost:3000/context/</loc>\n                        <lastmod>\\#(nowString)</lastmod>\n                        <loc>http://localhost:3000/about/</loc>\n                        <lastmod>\\#(nowString)</lastmod>\n                        <loc>http://localhost:3000/</loc>\n                        <lastmod>\\#(nowString)</lastmod>\n\n                        <loc>http://localhost:3000/blog/posts/post-1/</loc>\n                        <lastmod>\\#(nowString)</lastmod>\n                        <loc>http://localhost:3000/blog/posts/post-2/</loc>\n                        <lastmod>\\#(nowString)</lastmod>\n                        <loc>http://localhost:3000/blog/posts/post-3/</loc>\n                        <lastmod>\\#(nowString)</lastmod>\n\n                        <loc>http://localhost:3000/blog/authors/author-3/</loc>\n                        <lastmod>\\#(nowString)</lastmod>\n                        <loc>http://localhost:3000/blog/authors/author-2/</loc>\n                        <lastmod>\\#(nowString)</lastmod>\n                        <loc>http://localhost:3000/blog/authors/author-1/</loc>\n                        <lastmod>\\#(nowString)</lastmod>\n\n                        <loc>http://localhost:3000/blog/tags/tag-3/</loc>\n                        <lastmod>\\#(nowString)</lastmod>\n                        <loc>http://localhost:3000/blog/tags/tag-2/</loc>\n                        <lastmod>\\#(nowString)</lastmod>\n                        <loc>http://localhost:3000/blog/tags/tag-1/</loc>\n                        <lastmod>\\#(nowString)</lastmod>\n\n\n\n                    </url>\n                </urlset>\n                \"\"\"#\n\n            #expect(\n                sitemap.trimmingCharacters(in: .whitespacesAndNewlines)\n                    == expectation.trimmingCharacters(\n                        in: .whitespacesAndNewlines\n                    )\n            )\n        }\n    }\n\n    @Test\n    func redirect() throws {\n        let now = Date()\n\n        try FileManagerPlayground {\n            Mocks.E2E.src(now: now)\n        }\n        .test {\n            let workDir = $1.appendingPathIfPresent(\"src\")\n            let toucan = Toucan()\n            try toucan.generate(\n                workDir: workDir.path(),\n                now: now\n            )\n\n            let distURL = workDir.appendingPathIfPresent(\"dist\")\n\n            let redirect1URL = distURL.appendingPathIfPresent(\n                \"redirects/home-old/index.html\"\n            )\n            let redirect1 = try String(\n                contentsOf: redirect1URL,\n                encoding: .utf8\n            )\n            let expectation1 = #\"\"\"\n                <!DOCTYPE html>\n                <html lang=\"en-US\">\n                  <meta charset=\"utf-8\">\n                  <title>Redirecting&hellip;</title>\n                  <link rel=\"canonical\" href=\"http://localhost:3000/\">\n                  <script>location=\"http://localhost:3000/\"</script>\n                  <meta http-equiv=\"refresh\" content=\"0; url=http://localhost:3000/\">\n                  <meta name=\"robots\" content=\"noindex\">\n                  <h1>Redirecting&hellip;</h1>\n                  <a href=\"http://localhost:3000/\">Click here if you are not redirected.</a>\n                </html>\n                \"\"\"#\n\n            #expect(\n                redirect1.trimmingCharacters(in: .whitespacesAndNewlines)\n                    == expectation1.trimmingCharacters(\n                        in: .whitespacesAndNewlines\n                    )\n            )\n\n            let redirect2URL = distURL.appendingPathIfPresent(\n                \"redirects/about-old/index.html\"\n            )\n            let redirect2 = try String(\n                contentsOf: redirect2URL,\n                encoding: .utf8\n            )\n            let expectation2 = #\"\"\"\n                <!DOCTYPE html>\n                <html lang=\"en-US\">\n                  <meta charset=\"utf-8\">\n                  <title>Redirecting&hellip;</title>\n                  <link rel=\"canonical\" href=\"http://localhost:3000/about\">\n                  <script>location=\"http://localhost:3000/about\"</script>\n                  <meta http-equiv=\"refresh\" content=\"0; url=http://localhost:3000/about\">\n                  <meta name=\"robots\" content=\"noindex\">\n                  <h1>Redirecting&hellip;</h1>\n                  <a href=\"http://localhost:3000/about\">Click here if you are not redirected.</a>\n                </html>\n                \"\"\"#\n\n            #expect(\n                redirect2.trimmingCharacters(in: .whitespacesAndNewlines)\n                    == expectation2.trimmingCharacters(\n                        in: .whitespacesAndNewlines\n                    )\n            )\n        }\n    }\n\n    // MARK: - other tests\n\n    @Test\n    func customContextViewForAllPipeline() throws {\n        let now = Date()\n\n        try FileManagerPlayground {\n            Mocks.E2E.src(\n                now: now,\n                debugContext: #\"\"\"\n                    {{page.description}}\n                    \"\"\"#\n            )\n        }\n        .test {\n            let workDir = $1.appendingPathIfPresent(\"src\")\n            let toucan = Toucan()\n            try toucan.generate(\n                workDir: workDir.path(),\n                now: now\n            )\n\n            let distURL = workDir.appendingPathIfPresent(\"dist\")\n            let htmlURL = distURL.appendingPathIfPresent(\"context/index.html\")\n            let html = try String(contentsOf: htmlURL, encoding: .utf8)\n            let exp = \"Context page description\"\n            #expect(html.trimmingCharacters(in: .whitespacesAndNewlines) == exp)\n        }\n    }\n\n    // MARK: - assets\n\n    private func mockSiteYAMLFile() -> YAMLFile<Settings> {\n        .init(\n            name: \"site\",\n            contents: Settings(\n                [\n                    \"name\": \"Test site name\",\n                    \"description\": \"Test site description\",\n                    \"language\": \"en-US\",\n                ]\n            )\n        )\n    }\n\n    private func mockTestTypes() -> Directory {\n        Directory(name: \"types\") {\n            YAMLFile(\n                name: \"test\",\n                contents: ContentType(\n                    id: \"test\",\n                    default: true\n                )\n            )\n        }\n    }\n\n    @Test\n    func loadOneSVGFile() async throws {\n        let now = Date()\n\n        try FileManagerPlayground {\n            Directory(name: \"src\") {\n                mockSiteYAMLFile()\n                Directory(name: \"pipelines\") {\n                    YAMLFile(\n                        name: \"test\",\n                        contents: Pipeline(\n                            id: \"test\",\n                            assets: .init(\n                                properties: [\n                                    .init(\n                                        action: .load,\n                                        property: \"icon\",\n                                        resolvePath: false,\n                                        input: .init(\n                                            path: nil,\n                                            name: \"icon\",\n                                            ext: \"svg\"\n                                        )\n                                    )\n                                ]\n                            ),\n                            engine: .init(\n                                id: \"json\",\n                                options: [\n                                    \"keyPath\": \"page\"\n                                ]\n                            ),\n                            output: .init(\n                                path: \"\",\n                                file: \"context\",\n                                ext: \"json\"\n                            )\n                        )\n                    )\n                }\n                mockTestTypes()\n                Directory(name: \"contents\") {\n                    RawContentBundle(\n                        name: \"test\",\n                        rawContent: .init(\n                            origin: .init(\n                                path: .init(\"test\"),\n                                slug: \"test\"\n                            ),\n                            markdown: .init(\n                                frontMatter: [\n                                    \"type\": \"test\"\n                                ]\n                            ),\n                            lastModificationDate: now.timeIntervalSince1970,\n                            assetsPath: \"assets\",\n                            assets: [\n                                \"icon.svg\",\n                                \"foo.svg\",\n                                \"bar.svg\",\n                            ]\n                        )\n                    )\n                }\n            }\n        }\n        .test {\n            let workDir = $1.appendingPathIfPresent(\"src\")\n            let toucan = Toucan()\n            try toucan.generate(\n                workDir: workDir.path(),\n                now: now\n            )\n\n            let distURL = workDir.appendingPathIfPresent(\"dist\")\n            let contextURL = distURL.appendingPathIfPresent(\"context.json\")\n            //            let context = try String(contentsOf: contextURL, encoding: .utf8)\n            let data = try Data(contentsOf: contextURL)\n\n            let decoder = JSONDecoder()\n\n            struct Exp: Decodable {\n                let icon: String\n            }\n\n            let exp = try decoder.decode(Exp.self, from: data)\n            #expect(exp.icon == \"icon.svg\")\n        }\n    }\n\n    @Test\n    func loadMultipleSVGFiles() async throws {\n        let now = Date()\n\n        try FileManagerPlayground {\n            Directory(name: \"src\") {\n                mockSiteYAMLFile()\n                Directory(name: \"pipelines\") {\n                    YAMLFile(\n                        name: \"test\",\n                        contents: Pipeline(\n                            id: \"test\",\n                            assets: .init(\n                                properties: [\n                                    .init(\n                                        action: .load,\n                                        property: \"icons\",\n                                        resolvePath: false,\n                                        input: .init(\n                                            path: nil,\n                                            name: \"*\",\n                                            ext: \"svg\"\n                                        )\n                                    )\n                                ]\n                            ),\n                            transformers: [:],\n                            engine: .init(\n                                id: \"json\",\n                                options: [\n                                    \"keyPath\": \"page\"\n                                ]\n                            ),\n                            output: .init(\n                                path: \"\",\n                                file: \"context\",\n                                ext: \"json\"\n                            )\n                        )\n                    )\n                }\n                mockTestTypes()\n                Directory(name: \"contents\") {\n                    RawContentBundle(\n                        name: \"test\",\n                        rawContent: .init(\n                            origin: .init(\n                                path: .init(\"test\"),\n                                slug: \"test\"\n                            ),\n                            markdown: .init(\n                                frontMatter: [\n                                    \"type\": \"test\"\n                                ]\n                            ),\n                            lastModificationDate: now.timeIntervalSince1970,\n                            assetsPath: \"assets\",\n                            assets: [\n                                \"cover.jpg\",\n                                \"foo.svg\",\n                                \"bar.svg\",\n                            ]\n                        )\n                    )\n                }\n            }\n        }\n        .test {\n            let workDir = $1.appendingPathIfPresent(\"src\")\n            let toucan = Toucan()\n            try toucan.generate(\n                workDir: workDir.path(),\n                now: now\n            )\n\n            let distURL = workDir.appendingPathIfPresent(\"dist\")\n            let contextURL = distURL.appendingPathIfPresent(\"context.json\")\n            //            let context = try String(contentsOf: contextURL, encoding: .utf8)\n            let data = try Data(contentsOf: contextURL)\n\n            let decoder = JSONDecoder()\n\n            struct Exp: Decodable {\n                let icons: [String: String]\n            }\n\n            let exp = try decoder.decode(Exp.self, from: data)\n\n            #expect(exp.icons.keys.sorted() == [\"foo\", \"bar\"].sorted())\n            #expect(\n                exp.icons.values.sorted()\n                    == [\n                        \"foo.svg\",\n                        \"bar.svg\",\n                    ]\n                    .sorted()\n            )\n        }\n    }\n\n    @Test\n    func parseOneDataFile() async throws {\n        let now = Date()\n\n        try FileManagerPlayground {\n            Directory(name: \"src\") {\n                mockSiteYAMLFile()\n                Directory(name: \"pipelines\") {\n                    YAMLFile(\n                        name: \"test\",\n                        contents: Pipeline(\n                            id: \"test\",\n                            assets: .init(\n                                properties: [\n                                    .init(\n                                        action: .parse,\n                                        property: \"data\",\n                                        resolvePath: false,\n                                        input: .init(\n                                            path: nil,\n                                            name: \"data\",\n                                            ext: \"yaml\"\n                                        )\n                                    )\n                                ]\n                            ),\n                            engine: .init(\n                                id: \"json\",\n                                options: [\n                                    \"keyPath\": \"page\"\n                                ]\n                            ),\n                            output: .init(\n                                path: \"\",\n                                file: \"context\",\n                                ext: \"json\"\n                            )\n                        )\n                    )\n                }\n                mockTestTypes()\n                Directory(name: \"contents\") {\n                    Directory(name: \"test\") {\n                        Directory(name: \"assets\") {\n                            File(\n                                name: \"data.yaml\",\n                                string: \"\"\"\n                                    foo: value1\n                                    bar: value2\n                                    \"\"\"\n                            )\n                        }\n                        MarkdownFile(\n                            name: \"index\",\n                            markdown: .init(\n                                frontMatter: [\n                                    \"type\": \"test\"\n                                ]\n                            )\n                        )\n                    }\n                }\n            }\n        }\n        .test {\n            let workDir = $1.appendingPathIfPresent(\"src\")\n            let toucan = Toucan()\n            try toucan.generate(\n                workDir: workDir.path(),\n                now: now\n            )\n\n            let distURL = workDir.appendingPathIfPresent(\"dist\")\n            let contextURL = distURL.appendingPathIfPresent(\"context.json\")\n            //            let context = try String(contentsOf: contextURL, encoding: .utf8)\n            let data = try Data(contentsOf: contextURL)\n\n            let decoder = JSONDecoder()\n\n            struct Exp: Decodable {\n                let data: [String: String]\n            }\n\n            let exp = try decoder.decode(Exp.self, from: data)\n            #expect(exp.data[\"foo\"] == \"value1\")\n            #expect(exp.data[\"bar\"] == \"value2\")\n        }\n    }\n\n    @Test\n    func parseMultipleDataFile() async throws {\n        let now = Date()\n\n        try FileManagerPlayground {\n            Directory(name: \"src\") {\n                mockSiteYAMLFile()\n                Directory(name: \"pipelines\") {\n                    YAMLFile(\n                        name: \"test\",\n                        contents: Pipeline(\n                            id: \"test\",\n                            assets: .init(\n                                properties: [\n                                    .init(\n                                        action: .parse,\n                                        property: \"data\",\n                                        resolvePath: false,\n                                        input: .init(\n                                            path: nil,\n                                            name: \"*\",\n                                            ext: \"yaml\"\n                                        )\n                                    )\n                                ]\n                            ),\n                            engine: .init(\n                                id: \"json\",\n                                options: [\n                                    \"keyPath\": \"page\"\n                                ]\n                            ),\n                            output: .init(\n                                path: \"\",\n                                file: \"context\",\n                                ext: \"json\"\n                            )\n                        )\n                    )\n                }\n                mockTestTypes()\n                Directory(name: \"contents\") {\n                    Directory(name: \"test\") {\n                        Directory(name: \"assets\") {\n                            File(\n                                name: \"foo.yaml\",\n                                string: \"\"\"\n                                    foo: value1\n                                    \"\"\"\n                            )\n                            File(\n                                name: \"bar.yaml\",\n                                string: \"\"\"\n                                    bar: value2\n                                    \"\"\"\n                            )\n                        }\n                        MarkdownFile(\n                            name: \"index\",\n                            markdown: .init(\n                                frontMatter: [\n                                    \"type\": \"test\"\n                                ]\n                            )\n                        )\n                    }\n                }\n            }\n        }\n        .test {\n            let workDir = $1.appendingPathIfPresent(\"src\")\n            let toucan = Toucan()\n            try toucan.generate(\n                workDir: workDir.path(),\n                now: now\n            )\n\n            let distURL = workDir.appendingPathIfPresent(\"dist\")\n            let contextURL = distURL.appendingPathIfPresent(\"context.json\")\n            //            let context = try String(contentsOf: contextURL, encoding: .utf8)\n            let data = try Data(contentsOf: contextURL)\n\n            let decoder = JSONDecoder()\n\n            struct Exp: Decodable {\n                let data: [String: [String: String]]\n            }\n\n            let exp = try decoder.decode(Exp.self, from: data)\n            #expect(exp.data[\"foo\"]?[\"foo\"] == \"value1\")\n            #expect(exp.data[\"bar\"]?[\"bar\"] == \"value2\")\n        }\n    }\n\n    // MARK: - asset behaviors\n\n    @Test\n    func minifyCSSAsset() async throws {\n        let now = Date()\n\n        try FileManagerPlayground {\n            Directory(name: \"src\") {\n                mockSiteYAMLFile()\n                Directory(name: \"pipelines\") {\n                    YAMLFile(\n                        name: \"test\",\n                        contents: Pipeline(\n                            id: \"test\",\n                            assets: .init(\n                                behaviors: [\n                                    .init(\n                                        id: \"minify-css\",\n                                        input: .init(\n                                            name: \"style\",\n                                            ext: \"css\"\n                                        ),\n                                        output: .init(\n                                            name: \"style.min\",\n                                            ext: \"css\"\n                                        )\n                                    )\n                                ]\n                            ),\n                            engine: .init(\n                                id: \"json\",\n                                options: [\n                                    \"keyPath\": \"page\"\n                                ]\n                            ),\n                            output: .init(\n                                path: \"\",\n                                file: \"context\",\n                                ext: \"json\"\n                            )\n                        )\n                    )\n                }\n                mockTestTypes()\n                Directory(name: \"contents\") {\n                    Directory(name: \"test\") {\n                        Directory(name: \"assets\") {\n                            File(\n                                name: \"style.css\",\n                                string: \"\"\"\n                                    html {\n                                        margin: 0;\n                                        padding: 0;\n                                    }\n                                    body {\n                                        background: red;\n                                    }\n                                    \"\"\"\n                            )\n                        }\n                        MarkdownFile(\n                            name: \"index\",\n                            markdown: .init(\n                                frontMatter: [\n                                    \"type\": \"test\"\n                                ]\n                            )\n                        )\n                    }\n                }\n            }\n        }\n        .test {\n            let workDir = $1.appendingPathIfPresent(\"src\")\n            let toucan = Toucan()\n            try toucan.generate(\n                workDir: workDir.path(),\n                now: now\n            )\n\n            let distURL = workDir.appendingPathIfPresent(\"dist\")\n            let cssURL = distURL.appendingPathIfPresent(\n                \"assets/test/style.min.css\"\n            )\n\n            let css = try String(contentsOf: cssURL, encoding: .utf8)\n\n            #expect(\n                css.contains(\n                    \"html{margin:0;padding:0}body{background:red}\"\n                )\n            )\n        }\n    }\n\n    @Test\n    func sASSAsset() async throws {\n        let now = Date()\n\n        try FileManagerPlayground {\n            Directory(name: \"src\") {\n                mockSiteYAMLFile()\n                Directory(name: \"pipelines\") {\n                    YAMLFile(\n                        name: \"test\",\n                        contents: Pipeline(\n                            id: \"test\",\n                            assets: .init(\n                                behaviors: [\n                                    .init(\n                                        id: \"compile-sass\",\n                                        input: .init(\n                                            name: \"style\",\n                                            ext: \"sass\"\n                                        ),\n                                        output: .init(\n                                            name: \"style\",\n                                            ext: \"css\"\n                                        )\n                                    )\n                                ]\n                            ),\n                            engine: .init(\n                                id: \"json\",\n                                options: [\n                                    \"keyPath\": \"page\"\n                                ]\n                            ),\n                            output: .init(\n                                path: \"\",\n                                file: \"context\",\n                                ext: \"json\"\n                            )\n                        )\n                    )\n                }\n                mockTestTypes()\n                Directory(name: \"contents\") {\n                    Directory(name: \"test\") {\n                        Directory(name: \"assets\") {\n                            File(\n                                name: \"style.sass\",\n                                string: \"\"\"\n                                    $font-stack: Helvetica, sans-serif\n                                    $primary-color: #333\n\n                                    body\n                                      font: 100% $font-stack\n                                      color: $primary-color\n                                    \"\"\"\n                            )\n                        }\n                        MarkdownFile(\n                            name: \"index\",\n                            markdown: .init(\n                                frontMatter: [\n                                    \"type\": \"test\"\n                                ]\n                            )\n                        )\n                    }\n                }\n            }\n        }\n        .test {\n            let workDir = $1.appendingPathIfPresent(\"src\")\n            let toucan = Toucan()\n            try toucan.generate(\n                workDir: workDir.path(),\n                now: now\n            )\n\n            let distURL = workDir.appendingPathIfPresent(\"dist\")\n            let cssURL = distURL.appendingPathIfPresent(\n                \"assets/test/style.css\"\n            )\n\n            let css = try String(contentsOf: cssURL, encoding: .utf8)\n\n            #expect(\n                css.contains(\n                    \"\"\"\n                    body {\n                      font: 100% Helvetica, sans-serif;\n                      color: #333;\n                    }\n                    \"\"\"\n                )\n            )\n        }\n    }\n\n    @Test\n    func sCSSModuleLoader() async throws {\n        let now = Date()\n\n        try FileManagerPlayground {\n            Directory(name: \"src\") {\n                mockSiteYAMLFile()\n                Directory(name: \"pipelines\") {\n                    YAMLFile(\n                        name: \"test\",\n                        contents: Pipeline(\n                            id: \"test\",\n                            assets: .init(\n                                behaviors: [\n                                    .init(\n                                        id: \"compile-sass\",\n                                        input: .init(\n                                            name: \"style\",\n                                            ext: \"scss\"\n                                        ),\n                                        output: .init(\n                                            name: \"style\",\n                                            ext: \"css\"\n                                        )\n                                    )\n                                ]\n                            ),\n                            engine: .init(\n                                id: \"json\",\n                                options: [\n                                    \"keyPath\": \"page\"\n                                ]\n                            ),\n                            output: .init(\n                                path: \"\",\n                                file: \"context\",\n                                ext: \"json\"\n                            )\n                        )\n                    )\n                }\n                mockTestTypes()\n                Directory(name: \"contents\") {\n                    Directory(name: \"test\") {\n                        Directory(name: \"assets\") {\n                            File(\n                                name: \"_colors.scss\",\n                                string: \"\"\"\n                                    $primary: blue;\n                                    \"\"\"\n                            )\n                            File(\n                                name: \"style.scss\",\n                                string: \"\"\"\n                                    @use \"colors\";\n\n                                    body {\n                                      color: colors.$primary;\n                                    }\n                                    \"\"\"\n                            )\n                        }\n                        MarkdownFile(\n                            name: \"index\",\n                            markdown: .init(\n                                frontMatter: [\n                                    \"type\": \"test\"\n                                ]\n                            )\n                        )\n                    }\n                }\n            }\n        }\n        .test {\n            let workDir = $1.appendingPathIfPresent(\"src\")\n            let toucan = Toucan()\n            try toucan.generate(\n                workDir: workDir.path(),\n                now: now\n            )\n\n            let distURL = workDir.appendingPathIfPresent(\"dist\")\n            let cssURL = distURL.appendingPathIfPresent(\n                \"assets/test/style.css\"\n            )\n\n            let css = try String(contentsOf: cssURL, encoding: .utf8)\n\n            #expect(\n                css.contains(\n                    \"\"\"\n                    body {\n                      color: blue;\n                    }\n                    \"\"\"\n                )\n            )\n        }\n    }\n\n    // MARK: - custom view\n\n    @Test\n    func customView() async throws {\n        let now = Date()\n\n        try FileManagerPlayground {\n            Directory(name: \"src\") {\n                mockSiteYAMLFile()\n                Directory(name: \"pipelines\") {\n                    YAMLFile(\n                        name: \"html\",\n                        contents: Pipeline(\n                            id: \"html\",\n                            engine: .init(\n                                id: \"mustache\",\n                                options: [\n                                    \"contentTypes\": [\n                                        \"test\": [\n                                            ViewFrontMatterKeys.view.rawValue:\n                                                \"foo\"\n                                        ]\n                                    ]\n                                ]\n                            ),\n                            output: .init(\n                                path: \"{{slug}}\",\n                                file: \"index\",\n                                ext: \"html\"\n                            )\n                        )\n                    )\n                }\n                mockTestTypes()\n                Directory(name: \"contents\") {\n                    Directory(name: \"test\") {\n                        File(\n                            name: \"index.yaml\",\n                            string: \"\"\"\n                                views:\n                                    html: bar\n                                \"\"\"\n                        )\n                    }\n                }\n                Directory(name: \"templates\") {\n                    Directory(name: \"default\") {\n                        YAMLFile(\n                            name: \"template\",\n                            contents: Mocks.Templates.metadata()\n                        )\n                        Directory(name: \"views\") {\n                            MustacheFile(\n                                name: \"foo\",\n                                contents: \"\"\"\n                                    foo\n                                    \"\"\"\n                            )\n                            MustacheFile(\n                                name: \"bar\",\n                                contents: \"\"\"\n                                    bar\n                                    \"\"\"\n                            )\n                        }\n                    }\n                }\n            }\n        }\n        .test {\n            let workDir = $1.appendingPathIfPresent(\"src\")\n            let toucan = Toucan()\n            try toucan.generate(\n                workDir: workDir.path(),\n                now: now\n            )\n\n            let distURL = workDir.appendingPathIfPresent(\"dist\")\n\n            let fileURL = distURL.appendingPathIfPresent(\"test/index.html\")\n            let html = try String(contentsOf: fileURL, encoding: .utf8)\n\n            #expect(html.contains(\"bar\"))\n        }\n    }\n\n    @Test\n    func optionalArrayItems() async throws {\n        let now = Date()\n\n        try FileManagerPlayground {\n            Directory(name: \"src\") {\n                mockSiteYAMLFile()\n                Directory(name: \"pipelines\") {\n                    YAMLFile(\n                        name: \"html\",\n                        contents: Pipeline(\n                            id: \"html\",\n                            engine: .init(\n                                id: \"mustache\",\n                                options: [\n                                    \"contentTypes\": [\n                                        \"test\": [\n                                            \"view\": \"foo\"\n                                        ]\n                                    ]\n                                ]\n                            ),\n                            output: .init(\n                                path: \"{{slug}}\",\n                                file: \"index\",\n                                ext: \"html\"\n                            )\n                        )\n                    )\n                }\n                mockTestTypes()\n                Directory(name: \"contents\") {\n                    Directory(name: \"test\") {\n                        File(\n                            name: \"index.yaml\",\n                            string: \"\"\"\n                                foo:\n                                    bar:\n                                        baz:\n                                            title: \"asdf\"\n                                            bug:\n                                                - this\n                                                - is \n                                                - not\n                                                - ok\n                                \"\"\"\n                        )\n                    }\n                }\n                Directory(name: \"templates\") {\n                    Directory(name: \"default\") {\n                        YAMLFile(\n                            name: \"template\",\n                            contents: Mocks.Templates.metadata()\n                        )\n                        Directory(name: \"views\") {\n                            MustacheFile(\n                                name: \"foo\",\n                                contents: \"\"\"\n                                    {{#page.foo.bar.baz.bug}}{{.}}{{/page.foo.bar.baz.bug}}\n                                    \"\"\"\n                            )\n                        }\n                    }\n                }\n            }\n        }\n        .test {\n            let workDir = $1.appendingPathIfPresent(\"src\")\n            let toucan = Toucan()\n            try toucan.generate(\n                workDir: workDir.path(),\n                now: now\n            )\n\n            let distURL = workDir.appendingPathIfPresent(\"dist\")\n\n            let fileURL = distURL.appendingPathIfPresent(\"test/index.html\")\n            let html = try String(contentsOf: fileURL, encoding: .utf8)\n\n            #expect(\n                html.trimmingCharacters(\n                    in: .whitespacesAndNewlines\n                ) == \"thisisnotok\"\n            )\n        }\n    }\n\n    // MARK: - transformers\n\n    @Test\n    func transformerExecution() async throws {\n        let now = Date()\n        let fileManager = FileManager.default\n        let rootURL = FileManager.default.temporaryDirectory\n        let rootName = \"FileManagerPlayground_\\(UUID().uuidString)\"\n\n        try FileManagerPlayground(\n            rootUrl: rootURL,\n            rootName: rootName,\n            fileManager: fileManager\n        ) {\n            Directory(name: \"src\") {\n                mockSiteYAMLFile()\n                Directory(name: \"pipelines\") {\n                    YAMLFile(\n                        name: \"test\",\n                        contents: Pipeline(\n                            id: \"test\",\n                            transformers: [\n                                \"test\": .init(\n                                    run: [\n                                        .init(\n                                            path:\n                                                \"\\(rootURL.path())/\\(rootName)/src/transformers\",\n                                            name: \"replace\"\n                                        )\n                                    ],\n                                    isMarkdownResult: false\n                                )\n                            ],\n                            engine: .init(\n                                id: \"mustache\",\n                                options: [\n                                    \"contentTypes\": [\n                                        \"test\": [\n                                            ViewFrontMatterKeys.view.rawValue:\n                                                \"test\"\n                                        ]\n                                    ]\n                                ]\n                            ),\n                            output: .init(\n                                path: \"{{slug}}\",\n                                file: \"index\",\n                                ext: \"html\"\n                            )\n                        )\n                    )\n                }\n                mockTestTypes()\n                Directory(name: \"contents\") {\n                    Directory(name: \"test\") {\n                        File(\n                            name: \"index.yaml\",\n                            string: \"\"\"\n                                type: test\n                                description: Desc1\n                                label: label1\n                                \"\"\"\n                        )\n                        File(\n                            name: \"index.md\",\n                            string: \"\"\"\n                                ---\n                                title: \"First beta release\"\n                                ---\n                                Character to replace => :\n                                \"\"\"\n                        )\n                    }\n                }\n                Directory(name: \"transformers\") {\n                    File(\n                        name: \"replace\",\n                        attributes: [.posixPermissions: 0o777],\n                        string: \"\"\"\n                            #!/bin/bash\n                            # Replaces all colons `:` with dashes `-` in the given file.\n                            # Usage: replace-char --file <path>\n                            UNKNOWN_ARGS=()\n                            while [[ $# -gt 0 ]]; do\n                                case $1 in\n                                    --file)\n                                        TOUCAN_FILE=\"$2\"\n                                        shift\n                                        shift\n                                        ;;\n                                    -*|--*)\n                                        UNKNOWN_ARGS+=(\"$1\" \"$2\")\n                                        shift\n                                        shift\n                                        ;;\n                                    *)\n                                        shift\n                                        ;;\n                                esac\n                            done\n                            if [[ -z \"${TOUCAN_FILE}\" ]]; then\n                                echo \"❌ No file specified with --file.\"\n                                exit 1\n                            fi\n                            echo \"📄 Processing file: ${TOUCAN_FILE}\"\n                            if [[ ${#UNKNOWN_ARGS[@]} -gt 0 ]]; then\n                                echo \"ℹ️ Ignored unknown options: ${UNKNOWN_ARGS[*]}\"\n                            fi\n                            sed 's/:/-/g' \"${TOUCAN_FILE}\" > \"${TOUCAN_FILE}.tmp\" && mv \"${TOUCAN_FILE}.tmp\" \"${TOUCAN_FILE}\"\n                            echo \"✅ Done replacing characters.\"\n                            \"\"\"\n                    )\n                }\n                Mocks.E2E.templates(debugContext: \"{{.}}\")\n            }\n        }\n        .test {\n            let workDir = $1.appendingPathIfPresent(\"src\")\n            let toucan = Toucan()\n            try toucan.generate(\n                workDir: workDir.path(),\n                now: now\n            )\n\n            let distURL = workDir.appendingPathIfPresent(\"dist\")\n\n            let fileURL = distURL.appendingPathIfPresent(\"test/index.html\")\n            let html = try String(contentsOf: fileURL, encoding: .utf8)\n\n            #expect(html.contains(\"Character to replace => -\"))\n        }\n    }\n\n    @Test\n    func paginationPages() throws {\n        let now = Date()\n\n        try FileManagerPlayground {\n            Mocks.E2E.src(now: now)\n        }\n        .test {\n            let workDir = $1.appendingPathIfPresent(\"src\")\n            let toucan = Toucan()\n            try toucan.generate(\n                workDir: workDir.path(),\n                now: now\n            )\n\n            let distURL = workDir.appendingPathIfPresent(\"dist\")\n\n            let fileURL1 = distURL.appendingPathIfPresent(\n                \"blog/posts/pages/1/index.html\"\n            )\n            let html1 = try String(contentsOf: fileURL1, encoding: .utf8)\n            #expect(html1.contains(\"<title>Post pagination page 1 / 2</title>\"))\n            #expect(html1.contains(\"<h1>Post pagination page 1 / 2</h1>\"))\n\n            let fileURL2 = distURL.appendingPathIfPresent(\n                \"blog/posts/pages/2/index.html\"\n            )\n            let html2 = try String(contentsOf: fileURL2, encoding: .utf8)\n\n            #expect(html2.contains(\"<title>Post pagination page 2 / 2</title>\"))\n            #expect(html2.contains(\"<h1>Post pagination page 2 / 2</h1>\"))\n        }\n    }\n\n    @Test\n    func scopeBasics() async throws {\n        let now = Date()\n\n        try FileManagerPlayground {\n            Directory(name: \"src\") {\n                mockSiteYAMLFile()\n                Directory(name: \"pipelines\") {\n                    YAMLFile(\n                        name: \"test\",\n                        contents: Pipeline(\n                            id: \"test\",\n                            scopes: [\n                                \"test\": [\n                                    \"minimal\": .init(\n                                        context: .properties,\n                                        fields: [\n                                            \"slug\"\n                                        ]\n                                    ),\n                                    Pipeline.Scope.Keys.list.rawValue: .init(\n                                        context: .detail,\n                                        fields: [\n                                            \"title\",\n                                            \"slug\",\n                                        ]\n                                    ),\n                                ]\n                            ],\n                            queries: [\n                                \"minimal\": .init(\n                                    contentType: \"test\",\n                                    scope: \"minimal\"\n                                )\n                            ],\n                            engine: .init(\n                                id: \"json\"\n                            ),\n                            output: .init(\n                                path: \"\",\n                                file: \"context\",\n                                ext: \"json\"\n                            )\n                        )\n                    )\n                }\n                mockTestTypes()\n                Directory(name: \"contents\") {\n                    Directory(name: \"test\") {\n                        MarkdownFile(\n                            name: \"index\",\n                            markdown: .init(\n                                frontMatter: [\n                                    \"title\": \"Test\",\n                                    \"type\": \"test\",\n                                    \"foo\": \"bar\",\n                                ]\n                            )\n                        )\n                    }\n                }\n            }\n        }\n        .test {\n            let workDir = $1.appendingPathIfPresent(\"src\")\n            let toucan = Toucan()\n            try toucan.generate(\n                workDir: workDir.path(),\n                now: now\n            )\n\n            let distURL = workDir.appendingPathIfPresent(\"dist\")\n            let contextURL = distURL.appendingPathIfPresent(\"context.json\")\n            let data = try Data(contentsOf: contextURL)\n\n            let decoder = JSONDecoder()\n\n            struct Exp: Decodable {\n                struct Page: Decodable {\n                    let slug: String\n                    let title: String\n                }\n\n                struct Context: Decodable {\n                    struct Minimal: Decodable {\n                        let slug: String\n                    }\n\n                    let minimal: [Minimal]\n                }\n\n                let page: Page\n                let context: Context\n            }\n\n            let exp = try decoder.decode(Exp.self, from: data)\n            #expect(exp.page.title == \"Test\")\n            #expect(exp.page.slug == \"test\")\n\n            #expect(exp.context.minimal.count == 1)\n            let first = try #require(exp.context.minimal.first)\n            #expect(first.slug == \"test\")\n        }\n    }\n\n    @Test\n    func localizedDateOutputConfig() throws {\n        let now = Date()\n\n        try FileManagerPlayground {\n            Directory(name: \"src\") {\n                YAMLFile(\n                    name: \"toucan\",\n                    contents: TargetConfig(\n                        targets: [\n                            .standard\n                        ]\n                    )\n                )\n                YAMLFile(\n                    name: \"config\",\n                    contents: Config(\n                        site: .defaults,\n                        pipelines: .defaults,\n                        contents: .defaults,\n                        types: .defaults,\n                        blocks: .defaults,\n                        templates: .defaults,\n                        dataTypes: .init(\n                            date: .init(\n                                input: .defaults,\n                                output: .init(\n                                    locale: \"de-DE\",\n                                    timeZone: \"CET\"\n                                ),\n                                formats: [:]\n                            )\n                        ),\n                        renderer: .defaults\n                    )\n                )\n                YAMLFile(\n                    name: \"site\",\n                    contents: Settings(\n                        [\n                            \"name\": \"Test site name\",\n                            \"description\": \"Test site description\",\n                            \"language\": \"de-DE\",\n                        ]\n                    )\n                )\n                Directory(name: \"pipelines\") {\n                    YAMLFile(\n                        name: \"test\",\n                        contents: Pipeline(\n                            id: \"test\",\n                            engine: .init(\n                                id: \"json\",\n                                options: [:]\n                            ),\n                            output: .init(\n                                path: \"\",\n                                file: \"context\",\n                                ext: \"json\"\n                            )\n                        )\n                    )\n                }\n                Directory(name: \"types\") {\n                    YAMLFile(\n                        name: \"test\",\n                        contents: ContentType(\n                            id: \"test\",\n                            default: true,\n                            properties: [\n                                \"publication\": .init(\n                                    propertyType: .date(config: nil),\n                                    isRequired: true\n                                )\n                            ]\n                        )\n                    )\n                }\n                Directory(name: \"contents\") {\n                    Directory(name: \"test\") {\n                        MarkdownFile(\n                            name: \"index\",\n                            markdown: .init(\n                                frontMatter: [\n                                    \"title\": \"Test\",\n                                    \"type\": \"test\",\n                                    \"publication\": \"2025-03-30T09:23:14.870Z\",\n                                ]\n                            )\n                        )\n                    }\n                }\n            }\n        }\n        .test {\n            let workDir = $1.appendingPathIfPresent(\"src\")\n            let toucan = Toucan()\n            try toucan.generate(\n                workDir: workDir.path(),\n                now: now\n            )\n\n            let distURL = workDir.appendingPathIfPresent(\"dist\")\n            let contextURL = distURL.appendingPathIfPresent(\"context.json\")\n            let data = try Data(contentsOf: contextURL)\n\n            let decoder = JSONDecoder()\n\n            struct Exp: Decodable {\n                struct Page: Decodable {\n                    let slug: String\n                    let title: String\n                    let publication: DateContext\n                }\n                let page: Page\n            }\n\n            let exp = try decoder.decode(Exp.self, from: data)\n            #expect(exp.page.title == \"Test\")\n            #expect(exp.page.slug == \"test\")\n            #expect(exp.page.publication.date.full == \"Sonntag, 30. März 2025\")\n        }\n    }\n\n    @Test\n    func localizedDateOutputConfigPipelineOverride() throws {\n        let now = Date()\n\n        try FileManagerPlayground {\n            Directory(name: \"src\") {\n                YAMLFile(\n                    name: \"toucan\",\n                    contents: TargetConfig(\n                        targets: [\n                            .standard\n                        ]\n                    )\n                )\n                YAMLFile(\n                    name: \"config\",\n                    contents: Config(\n                        dataTypes: .init(\n                            date: .init(\n                                input: .defaults,\n                                output: .init(\n                                    locale: \"de-DE\",\n                                    timeZone: \"CET\"\n                                ),\n                                formats: [:]\n                            )\n                        ),\n                        renderer: .defaults\n                    )\n                )\n                YAMLFile(\n                    name: \"site\",\n                    contents: Settings(\n                        [\n                            \"name\": \"Test site name\",\n                            \"description\": \"Test site description\",\n                            \"language\": \"de-DE\",\n                        ]\n                    )\n                )\n                Directory(name: \"pipelines\") {\n                    YAMLFile(\n                        name: \"test\",\n                        contents: Pipeline(\n                            id: \"test\",\n                            dataTypes: .init(\n                                date: .init(\n                                    output: .init(\n                                        locale: \"hu-HU\",\n                                        timeZone: \"CET\"\n                                    ),\n                                    formats: [:]\n                                )\n                            ),\n                            engine: .init(\n                                id: \"json\",\n                                options: [:]\n                            ),\n                            output: .init(\n                                path: \"\",\n                                file: \"context\",\n                                ext: \"json\"\n                            )\n                        )\n                    )\n                }\n                Directory(name: \"types\") {\n                    YAMLFile(\n                        name: \"test\",\n                        contents: ContentType(\n                            id: \"test\",\n                            default: true,\n                            properties: [\n                                \"publication\": .init(\n                                    propertyType: .date(config: nil),\n                                    isRequired: true\n                                )\n                            ]\n                        )\n                    )\n                }\n                Directory(name: \"contents\") {\n                    Directory(name: \"test\") {\n                        MarkdownFile(\n                            name: \"index\",\n                            markdown: .init(\n                                frontMatter: [\n                                    \"title\": \"Test\",\n                                    \"type\": \"test\",\n                                    \"publication\": \"2025-03-30T09:23:14.870Z\",\n                                ]\n                            )\n                        )\n                    }\n                }\n            }\n        }\n        .test {\n            let workDir = $1.appendingPathIfPresent(\"src\")\n            let toucan = Toucan()\n            try toucan.generate(\n                workDir: workDir.path(),\n                now: now\n            )\n\n            let distURL = workDir.appendingPathIfPresent(\"dist\")\n            let contextURL = distURL.appendingPathIfPresent(\"context.json\")\n            let data = try Data(contentsOf: contextURL)\n\n            let decoder = JSONDecoder()\n\n            struct Exp: Decodable {\n                struct Page: Decodable {\n                    let slug: String\n                    let title: String\n                    let publication: DateContext\n                }\n                let page: Page\n            }\n\n            let exp = try decoder.decode(Exp.self, from: data)\n            #expect(exp.page.title == \"Test\")\n            #expect(exp.page.slug == \"test\")\n            #expect(\n                exp.page.publication.date.full == \"2025. március 30., vasárnap\"\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSDKTests/Files/MarkdownFile.swift",
    "content": "//\n//  MarkdownFile.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 06. 04..\n//\n\nimport FileManagerKit\nimport FileManagerKitBuilder\nimport ToucanSerialization\nimport ToucanSource\nimport Foundation\n\nstruct MarkdownFile {\n\n    var name: String\n    var ext: String\n    var markdown: Markdown\n    var modificationDate: Date\n\n    init(\n        name: String,\n        ext: String = \"md\",\n        markdown: Markdown,\n        modificationDate: Date = .now\n    ) {\n        self.name = name\n        self.ext = ext\n        self.markdown = markdown\n        self.modificationDate = modificationDate\n    }\n}\n\nextension MarkdownFile: BuildableItem {\n\n    func buildItem() -> FileManagerPlayground.Item {\n        let encoder = ToucanYAMLEncoder()\n        let yml = try! encoder.encode(markdown.frontMatter)\n        return .file(\n            .init(\n                name: name + \".\" + ext,\n                attributes: [.modificationDate: modificationDate],\n                string: \"\"\"\n                    ---\n                    \\(yml)\n                    ---\n                    \\(markdown.contents)\n                    \"\"\"\n            )\n        )\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSDKTests/Files/MustacheFile.swift",
    "content": "//\n//  MustacheFile.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 06. 04..\n//\n\nimport FileManagerKit\nimport FileManagerKitBuilder\nimport ToucanSerialization\n\nstruct MustacheFile {\n\n    var name: String\n    var ext: String\n    var contents: String\n\n    init(\n        name: String,\n        ext: String = \"mustache\",\n        contents: String\n    ) {\n        self.name = name\n        self.ext = ext\n        self.contents = contents\n    }\n}\n\nextension MustacheFile: BuildableItem {\n\n    func buildItem() -> FileManagerPlayground.Item {\n        .file(\n            .init(\n                name: name + \".\" + ext,\n                string: contents\n            )\n        )\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSDKTests/Files/RawContentBundle.swift",
    "content": "//\n//  RawContentBundle.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 06. 04..\n//\n\nimport FileManagerKit\nimport FileManagerKitBuilder\nimport ToucanSerialization\nimport ToucanSource\nimport Foundation\n\nstruct RawContentBundle {\n    var name: String\n    var rawContent: RawContent\n    var modificationDate: Date = Date()\n}\n\nextension RawContentBundle: BuildableItem {\n\n    func buildItem() -> FileManagerPlayground.Item {\n        .directory(\n            Directory(name: name) {\n                Directory(name: rawContent.assetsPath) {\n                    for asset in rawContent.assets {\n                        File(name: asset, string: asset)\n                    }\n                }\n                MarkdownFile(\n                    name: \"index\",\n                    markdown: rawContent.markdown,\n                    modificationDate: modificationDate\n                )\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSDKTests/Files/YAMLFile.swift",
    "content": "//\n//  YAMLFile.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 06. 04..\n//\n\nimport FileManagerKit\nimport FileManagerKitBuilder\nimport ToucanSerialization\n\nstruct YAMLFile<T: Encodable> {\n\n    var name: String\n    var ext: String\n    var contents: T\n\n    init(\n        name: String,\n        ext: String = \"yml\",\n        contents: T\n    ) {\n        self.name = name\n        self.ext = ext\n        self.contents = contents\n    }\n}\n\nextension YAMLFile: BuildableItem {\n\n    func buildItem() -> FileManagerPlayground.Item {\n        let encoder = ToucanYAMLEncoder()\n        return .file(\n            .init(\n                name: name + \".\" + ext,\n                string: try! encoder.encode(contents)\n            )\n        )\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSDKTests/Mocks/Mocks+Blocks.swift",
    "content": "//\n//  Mocks+Blocks.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 21..\n//\n\nimport ToucanSource\n\nextension Mocks.Blocks {\n    static func link() -> Block {\n        .init(\n            name: \"link\",\n            parameters: [\n                .init(\n                    label: \"url\",\n                    isRequired: true,\n                    defaultValue: \"\"\n                ),\n                .init(\n                    label: \"class\",\n                    isRequired: true,\n                    defaultValue: \"\"\n                ),\n                .init(\n                    label: \"target\",\n                    isRequired: true,\n                    defaultValue: \"_blank\"\n                ),\n            ],\n            requiresParentDirective: nil,\n            removesChildParagraph: true,\n            tag: \"a\",\n            attributes: [\n                .init(name: \"href\", value: \"{{url}}\"),\n                .init(name: \"target\", value: \"{{target}}\"),\n                .init(name: \"class\", value: \"{{class}}\"),\n            ],\n            output: nil\n        )\n    }\n\n    static func highlightedText(\n        id: Int\n    ) -> Block {\n        .init(\n            name: \"HighlightedText-\\(id)\",\n            parameters: nil,\n            requiresParentDirective: nil,\n            removesChildParagraph: nil,\n            tag: \"div\",\n            attributes: [\n                .init(\n                    name: \"class\",\n                    value: \"highlighted-text\"\n                )\n            ],\n            output: nil\n        )\n    }\n\n    static func faq() -> Block {\n        .init(\n            name: \"FAQ\",\n            parameters: nil,\n            requiresParentDirective: nil,\n            removesChildParagraph: nil,\n            tag: \"div\",\n            attributes: [\n                .init(name: \"class\", value: \"faq\")\n            ],\n            output: nil\n        )\n    }\n\n    static func badDirective() -> Block {\n        .init(\n            name: \"BAD\",\n            parameters: [\n                .init(\n                    label: \"label\",\n                    isRequired: true\n                )\n            ],\n            requiresParentDirective: \"true\",\n            removesChildParagraph: nil,\n            tag: \"div\",\n            attributes: [\n                .init(name: \"att\", value: \"none\")\n            ],\n            output: nil\n        )\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSDKTests/Mocks/Mocks+BuildTargetSources.swift",
    "content": "//\n//  Mocks+BuildTargetSources.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 21..\n//\n\nimport Foundation\nimport Testing\nimport ToucanSDK\nimport ToucanSource\n\nextension Mocks {\n    static func buildTargetSource(\n        location: URL = .init(filePath: \"\"),\n        now: Date,\n        target: Target = .standard,\n        config: Config = .defaults,\n        settings: Settings = .defaults\n    ) -> BuildTargetSource {\n        let formatter = ToucanInputDateFormatter(\n            dateConfig: config.dataTypes.date\n        )\n\n        let postType = Mocks.ContentTypes.post()\n\n        guard\n            case let .date(\n                publicationConfig\n            ) = postType.properties[\"publication\"]?.type\n        else {\n            fatalError(\n                \"Mock post type issue: publication is not a date property.\"\n            )\n        }\n        guard\n            case let .date(\n                expirationConfig\n            ) = postType.properties[\"expiration\"]?.type\n        else {\n            fatalError(\n                \"Mock post type issue: expiration is not a date property.\"\n            )\n        }\n\n        return .init(\n            locations: .init(\n                sourceURL: location,\n                config: config\n            ),\n            target: target,\n            config: config,\n            settings: settings,\n            pipelines: [\n                Mocks.Pipelines.html(),\n                Mocks.Pipelines.notFound(),\n                Mocks.Pipelines.redirect(),\n                Mocks.Pipelines.sitemap(),\n                Mocks.Pipelines.rss(),\n                Mocks.Pipelines.api(),\n            ],\n            types: [\n                Mocks.ContentTypes.page(),\n                postType,\n                Mocks.ContentTypes.author(),\n                Mocks.ContentTypes.tag(),\n                Mocks.ContentTypes.category(),\n                Mocks.ContentTypes.guide(),\n                Mocks.ContentTypes.redirect(),\n            ],\n            rawContents: [\n                Mocks.RawContents.homePage(now: now),\n                Mocks.RawContents.aboutPage(now: now),\n                Mocks.RawContents.contextPage(now: now),\n                Mocks.RawContents.notFoundPage(now: now),\n\n                Mocks.RawContents.page(id: 1, now: now),\n                Mocks.RawContents.page(id: 2, now: now),\n                Mocks.RawContents.page(id: 3, now: now),\n\n                Mocks.RawContents.redirectHome(now: now),\n                Mocks.RawContents.redirectAbout(now: now),\n\n                Mocks.RawContents.sitemapXML(now: now),\n                Mocks.RawContents.rssXML(now: now),\n\n                Mocks.RawContents.author(id: 1, age: 18, now: now),\n                Mocks.RawContents.author(id: 2, age: 21, now: now),\n                Mocks.RawContents.author(id: 3, age: 42, now: now),\n\n                Mocks.RawContents.tag(id: 1, now: now),\n                Mocks.RawContents.tag(id: 2, now: now),\n                Mocks.RawContents.tag(id: 3, now: now),\n\n                Mocks.RawContents.post(\n                    id: 1,\n                    now: now,\n                    // near past\n                    publication: formatter.string(\n                        from: now.addingTimeInterval(-86400),\n                        using: publicationConfig\n                    ),\n                    // near future\n                    expiration: formatter.string(\n                        from: now.addingTimeInterval(86400),\n                        using: expirationConfig\n                    ),\n                    featured: false,\n                    authorIDs: [1, 2],\n                    tagIDs: [1, 2]\n                ),\n                Mocks.RawContents.post(\n                    id: 2,\n                    now: now,\n                    // past\n                    publication: formatter.string(\n                        from: now.addingTimeInterval(-86400 * 2),\n                        using: publicationConfig\n                    ),\n                    // future\n                    expiration: formatter.string(\n                        from: now.addingTimeInterval(86400 * 2),\n                        using: expirationConfig\n                    ),\n                    featured: true,\n                    authorIDs: [1, 2, 3],\n                    tagIDs: [2]\n                ),\n                Mocks.RawContents.post(\n                    id: 3,\n                    now: now,\n                    // distant past\n                    publication: formatter.string(\n                        from: now.addingTimeInterval(-86400 * 3),\n                        using: publicationConfig\n                    ),\n                    // distant future\n                    expiration: formatter.string(\n                        from: now.addingTimeInterval(86400 * 3),\n                        using: expirationConfig\n                    ),\n                    featured: false,\n                    authorIDs: [2, 3],\n                    tagIDs: [2, 3]\n                ),\n                Mocks.RawContents.postPagination(now: now),\n\n                Mocks.RawContents.category(id: 1, now: now),\n                Mocks.RawContents.category(id: 2, now: now),\n                Mocks.RawContents.category(id: 3, now: now),\n\n                Mocks.RawContents.guide(id: 1, categoryID: 1, now: now),\n                Mocks.RawContents.guide(id: 2, categoryID: 1, now: now),\n                Mocks.RawContents.guide(id: 3, categoryID: 1, now: now),\n                Mocks.RawContents.guide(id: 4, categoryID: 2, now: now),\n                Mocks.RawContents.guide(id: 5, categoryID: 2, now: now),\n                Mocks.RawContents.guide(id: 6, categoryID: 2, now: now),\n                Mocks.RawContents.guide(id: 7, categoryID: 3, now: now),\n                Mocks.RawContents.guide(id: 8, categoryID: 3, now: now),\n                Mocks.RawContents.guide(id: 9, categoryID: 3, now: now),\n            ],\n            blockDirectives: [\n                Mocks.Blocks.link()\n            ]\n        )\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSDKTests/Mocks/Mocks+ContentTypes.swift",
    "content": "//\n//  Mocks+ContentTypes.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 21..\n//\n\nimport ToucanSource\n\nextension Mocks.ContentTypes {\n\n    static func page() -> ContentType {\n        .init(\n            id: \"page\",\n            default: true,\n            paths: [\n                \"pages\"\n            ],\n            properties: [\n                \"draft\": .init(\n                    propertyType: .bool,\n                    isRequired: true,\n                    defaultValue: false\n                ),\n                \"title\": .init(\n                    propertyType: .string,\n                    isRequired: true\n                ),\n                \"description\": .init(\n                    propertyType: .string,\n                    isRequired: true\n                ),\n            ],\n            relations: [:],\n            queries: [:]\n        )\n    }\n\n    static func author() -> ContentType {\n        .init(\n            id: \"author\",\n            paths: [\n                \"blog/authors\"\n            ],\n            properties: [\n                \"draft\": .init(\n                    propertyType: .bool,\n                    isRequired: true,\n                    defaultValue: false\n                ),\n                \"name\": .init(\n                    propertyType: .string,\n                    isRequired: true\n                ),\n                \"description\": .init(\n                    propertyType: .string,\n                    isRequired: false\n                ),\n                \"image\": .init(\n                    propertyType: .asset,\n                    isRequired: true\n                ),\n                \"age\": .init(\n                    propertyType: .int,\n                    isRequired: false\n                ),\n                \"height\": .init(\n                    propertyType: .double,\n                    isRequired: false\n                ),\n            ],\n            relations: [:],\n            queries: [\n                \"posts\": .init(\n                    contentType: \"post\",\n                    scope: Pipeline.Scope.Keys.list.rawValue,\n                    filter: .field(\n                        key: \"authors\",\n                        operator: .contains,\n                        value: .init(\"{{id}}\")\n                    ),\n                    orderBy: [\n                        .init(\n                            key: \"publication\",\n                            direction: .desc\n                        )\n                    ]\n                )\n            ]\n        )\n    }\n\n    static func tag() -> ContentType {\n        .init(\n            id: \"tag\",\n            paths: [\n                \"blog/tags\"\n            ],\n            properties: [\n                \"draft\": .init(\n                    propertyType: .bool,\n                    isRequired: true,\n                    defaultValue: false\n                ),\n                \"title\": .init(\n                    propertyType: .string,\n                    isRequired: true\n                ),\n                \"description\": .init(\n                    propertyType: .string,\n                    isRequired: true\n                ),\n            ],\n            relations: [:],\n            queries: [\n                \"posts\": .init(\n                    contentType: \"post\",\n                    scope: Pipeline.Scope.Keys.list.rawValue,\n                    filter: .field(\n                        key: \"tags\",\n                        operator: .contains,\n                        value: .init(\"{{id}}\")\n                    ),\n                    orderBy: [\n                        .init(\n                            key: \"publication\",\n                            direction: .desc\n                        )\n                    ]\n                )\n            ]\n        )\n    }\n\n    static func post() -> ContentType {\n        .init(\n            id: \"post\",\n            paths: [\n                \"blog/posts\"\n            ],\n            properties: [\n                \"draft\": .init(\n                    propertyType: .bool,\n                    isRequired: true,\n                    defaultValue: false\n                ),\n                \"title\": .init(\n                    propertyType: .string,\n                    isRequired: true\n                ),\n                \"description\": .init(\n                    propertyType: .string,\n                    isRequired: true\n                ),\n                \"publication\": .init(\n                    propertyType: .date(\n                        config: .init(\n                            localization: .defaults,\n                            format: \"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\"\n                        )\n                    ),\n                    isRequired: true\n                ),\n                \"expiration\": .init(\n                    propertyType: .date(\n                        config: .init(\n                            localization: .defaults,\n                            format: \"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\"\n                        )\n                    ),\n                    isRequired: true\n                ),\n                \"featured\": .init(\n                    propertyType: .bool,\n                    isRequired: true,\n                    defaultValue: false\n                ),\n                \"rating\": .init(\n                    propertyType: .double,\n                    isRequired: false\n                ),\n            ],\n            relations: [\n                \"authors\": .init(\n                    references: \"author\",\n                    relationType: .many,\n                    order: .init(\n                        key: \"name\",\n                        direction: .asc\n                    )\n                ),\n                \"tags\": .init(\n                    references: \"tag\",\n                    relationType: .many,\n                    order: .init(\n                        key: \"title\",\n                        direction: .asc\n                    )\n                ),\n            ],\n            queries: [\n                \"prev\": .init(\n                    contentType: \"post\",\n                    limit: 1,\n                    filter: .field(\n                        key: \"publication\",\n                        operator: .lessThan,\n                        value: .init(\"{{publication}}\")\n                    ),\n                    orderBy: [\n                        .init(\n                            key: \"publication\",\n                            direction: .desc\n                        )\n                    ]\n                ),\n\n                \"next\": .init(\n                    contentType: \"post\",\n                    limit: 1,\n                    filter: .field(\n                        key: \"publication\",\n                        operator: .greaterThan,\n                        value: .init(\"{{publication}}\")\n                    ),\n                    orderBy: [\n                        .init(\n                            key: \"publication\",\n                            direction: .asc\n                        )\n                    ]\n                ),\n                \"related\": .init(\n                    contentType: \"post\",\n                    scope: Pipeline.Scope.Keys.list.rawValue,\n                    limit: 4,\n                    filter: .and(\n                        [\n                            .field(\n                                key: \"id\",\n                                operator: .notEquals,\n                                value: .init(\"{{id}}\")\n                            ),\n                            .field(\n                                key: \"authors\",\n                                operator: .matching,\n                                value: .init(\"{{authors}}\")\n                            ),\n                        ]\n                    ),\n                    orderBy: []\n                ),\n\n                \"similar\": .init(\n                    contentType: \"post\",\n                    scope: Pipeline.Scope.Keys.list.rawValue,\n                    limit: 4,\n                    filter: .and(\n                        [\n                            .field(\n                                key: \"id\",\n                                operator: .notEquals,\n                                value: .init(\"{{id}}\")\n                            ),\n                            .field(\n                                key: \"tags\",\n                                operator: .matching,\n                                value: .init(\"{{tags}}\")\n                            ),\n                        ]\n                    ),\n                    orderBy: []\n                ),\n            ]\n        )\n    }\n\n    static func category() -> ContentType {\n        .init(\n            id: \"category\",\n            paths: [\n                \"docs/categories\"\n            ],\n            properties: [\n                \"draft\": .init(\n                    propertyType: .bool,\n                    isRequired: true,\n                    defaultValue: false\n                ),\n                \"title\": .init(\n                    propertyType: .string,\n                    isRequired: true\n                ),\n                \"description\": .init(\n                    propertyType: .string,\n                    isRequired: false\n                ),\n                \"order\": .init(\n                    propertyType: .int,\n                    isRequired: true,\n                    defaultValue: .init(100)\n                ),\n            ],\n            relations: [:],\n            queries: [\n                \"guides\": .init(\n                    contentType: \"guide\",\n                    scope: Pipeline.Scope.Keys.list.rawValue,\n                    filter: .field(\n                        key: \"category\",\n                        operator: .equals,\n                        value: .init(\"{{id}}\")\n                    ),\n                    orderBy: [\n                        .init(\n                            key: \"order\",\n                            direction: .asc\n                        )\n                    ]\n                )\n            ]\n        )\n    }\n\n    static func guide() -> ContentType {\n        .init(\n            id: \"guide\",\n            paths: [\n                \"docs/guides\"\n            ],\n            properties: [\n                \"draft\": .init(\n                    propertyType: .bool,\n                    isRequired: true,\n                    defaultValue: false\n                ),\n                \"title\": .init(\n                    propertyType: .string,\n                    isRequired: true\n                ),\n                \"description\": .init(\n                    propertyType: .string,\n                    isRequired: false\n                ),\n                \"order\": .init(\n                    propertyType: .int,\n                    isRequired: true,\n                    defaultValue: .init(100)\n                ),\n            ],\n            relations: [\n                \"category\": .init(\n                    references: \"category\",\n                    relationType: .one\n                )\n            ],\n            queries: [:]\n        )\n    }\n\n    static func redirect() -> ContentType {\n        .init(\n            id: \"redirect\",\n            paths: [],\n            properties: [\n                \"draft\": .init(\n                    propertyType: .bool,\n                    isRequired: true,\n                    defaultValue: false\n                ),\n                \"to\": .init(\n                    propertyType: .string,\n                    isRequired: true\n                ),\n                \"code\": .init(\n                    propertyType: .int,\n                    isRequired: true,\n                    defaultValue: .init(301)\n                ),\n            ],\n            relations: [:],\n            queries: [:]\n        )\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSDKTests/Mocks/Mocks+E2E.swift",
    "content": "//\n//  Mocks+E2E.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 06. 08..\n//\n\nimport FileManagerKitBuilder\nimport Foundation\nimport ToucanSDK\nimport ToucanSource\n\nextension Mocks.E2E {\n\n    static func types(\n        postType: ContentType\n    ) -> Directory {\n        Directory(name: \"types\") {\n            YAMLFile(\n                name: \"page\",\n                contents: Mocks.ContentTypes.page()\n            )\n            YAMLFile(\n                name: \"author\",\n                contents: Mocks.ContentTypes.author()\n            )\n            YAMLFile(\n                name: \"tag\",\n                contents: Mocks.ContentTypes.tag()\n            )\n            YAMLFile(\n                name: \"post\",\n                contents: postType\n            )\n            YAMLFile(\n                name: \"category\",\n                contents: Mocks.ContentTypes.category()\n            )\n            YAMLFile(\n                name: \"guide\",\n                contents: Mocks.ContentTypes.guide()\n            )\n        }\n    }\n\n    static func pipelines() -> Directory {\n        Directory(name: \"pipelines\") {\n            YAMLFile(\n                name: \"html\",\n                contents: Mocks.Pipelines.html()\n            )\n            YAMLFile(\n                name: \"not-found\",\n                contents: Mocks.Pipelines.notFound()\n            )\n            YAMLFile(\n                name: \"redirect\",\n                contents: Mocks.Pipelines.redirect()\n            )\n            YAMLFile(\n                name: \"sitemap\",\n                contents: Mocks.Pipelines.sitemap()\n            )\n            YAMLFile(\n                name: \"rss\",\n                contents: Mocks.Pipelines.rss()\n            )\n            YAMLFile(\n                name: \"api\",\n                contents: Mocks.Pipelines.api()\n            )\n        }\n    }\n\n    static func blocks() -> Directory {\n        Directory(name: \"blocks\") {\n            YAMLFile(\n                name: \"faq\",\n                contents: Mocks.Blocks.faq()\n            )\n        }\n    }\n\n    static func templates(\n        debugContext: String\n    ) -> Directory {\n        Directory(name: \"templates\") {\n            Directory(name: \"default\") {\n                YAMLFile(\n                    name: \"template\",\n                    contents: Mocks.Templates.metadata()\n                )\n                Directory(name: \"assets\") {\n                    Directory(name: \"css\") {\n                        File(\n                            name: \"template.css\",\n                            string: \"\"\"\n                                body { background: #000; }\n                                \"\"\"\n                        )\n                    }\n                }\n                Directory(name: \"views\") {\n                    MustacheFile(\n                        name: \"test\",\n                        contents: Mocks.Views.page()\n                    )\n                    Directory(name: \"docs\") {\n                        Directory(name: \"category\") {\n                            MustacheFile(\n                                name: \"default\",\n                                contents: Mocks.Views.category()\n                            )\n                        }\n                        Directory(name: \"guide\") {\n                            MustacheFile(\n                                name: \"default\",\n                                contents: Mocks.Views.guide()\n                            )\n                        }\n                    }\n                    Directory(name: \"pages\") {\n                        MustacheFile(\n                            name: \"default\",\n                            contents: Mocks.Views.page()\n                        )\n                        MustacheFile(\n                            name: \"404\",\n                            contents: Mocks.Views.notFound()\n                        )\n                        MustacheFile(\n                            name: \"context\",\n                            contents: Mocks.Views.context(\n                                value: debugContext\n                            )\n                        )\n                    }\n                    Directory(name: \"blog\") {\n                        Directory(name: \"tag\") {\n                            MustacheFile(\n                                name: \"default\",\n                                contents: Mocks.Views.tag()\n                            )\n                        }\n                        Directory(name: \"post\") {\n                            MustacheFile(\n                                name: \"default\",\n                                contents: Mocks.Views.post()\n                            )\n                        }\n                        Directory(name: \"author\") {\n                            MustacheFile(\n                                name: \"default\",\n                                contents: Mocks.Views.author()\n                            )\n                        }\n                    }\n                    Directory(name: \"partials\") {\n                        Directory(name: \"blog\") {\n                            MustacheFile(\n                                name: \"author\",\n                                contents: Mocks.Views.partialAuthor()\n                            )\n                            MustacheFile(\n                                name: \"tag\",\n                                contents: Mocks.Views.partialTag()\n                            )\n                            MustacheFile(\n                                name: \"post\",\n                                contents: Mocks.Views.partialPost()\n                            )\n                        }\n                        Directory(name: \"docs\") {\n                            MustacheFile(\n                                name: \"category\",\n                                contents: Mocks.Views.partialCategory()\n                            )\n                            MustacheFile(\n                                name: \"guide\",\n                                contents: Mocks.Views.partialGuide()\n                            )\n                        }\n                    }\n                    MustacheFile(\n                        name: \"html\",\n                        contents: Mocks.Views.html()\n                    )\n                    MustacheFile(\n                        name: \"redirect\",\n                        contents: Mocks.Views.redirect()\n                    )\n                    MustacheFile(\n                        name: \"rss\",\n                        contents: Mocks.Views.rss()\n                    )\n                    MustacheFile(\n                        name: \"sitemap\",\n                        contents: Mocks.Views.sitemap()\n                    )\n                }\n            }\n        }\n    }\n\n    static func src(\n        now: Date,\n        debugContext: String = \"{{.}}\"\n    ) -> Directory {\n        let config: Config = .defaults\n\n        let formatter = ToucanInputDateFormatter(\n            dateConfig: config.dataTypes.date\n        )\n\n        let postType = Mocks.ContentTypes.post()\n\n        guard\n            case let .date(\n                publicationConfig\n            ) = postType.properties[\"publication\"]?.type\n        else {\n            fatalError(\n                \"Mock post type issue: publication is not a date property.\"\n            )\n        }\n        guard\n            case let .date(\n                expirationConfig\n            ) = postType.properties[\"expiration\"]?.type\n        else {\n            fatalError(\n                \"Mock post type issue: expiration is not a date property.\"\n            )\n        }\n\n        return Directory(name: \"src\") {\n            YAMLFile(\n                name: \"site\",\n                contents: [\n                    \"name\": \"Test site name\",\n                    \"description\": \"Test site description\",\n                    \"language\": \"en-US\",\n                ] as [String: AnyCodable]\n            )\n            Directory(name: \"contents\") {\n                RawContentBundle(\n                    name: \"\",\n                    rawContent: Mocks.RawContents.homePage(now: now)\n                )\n                RawContentBundle(\n                    name: \"about\",\n                    rawContent: Mocks.RawContents.aboutPage(now: now)\n                )\n                RawContentBundle(\n                    name: \"context\",\n                    rawContent: Mocks.RawContents.contextPage(now: now)\n                )\n                RawContentBundle(\n                    name: \"404\",\n                    rawContent: Mocks.RawContents.notFoundPage(now: now)\n                )\n\n                Directory(name: \"pages\") {\n                    RawContentBundle(\n                        name: \"page-1\",\n                        rawContent: Mocks.RawContents.page(id: 1, now: now)\n                    )\n                    RawContentBundle(\n                        name: \"page-2\",\n                        rawContent: Mocks.RawContents.page(id: 2, now: now)\n                    )\n                    RawContentBundle(\n                        name: \"page-3\",\n                        rawContent: Mocks.RawContents.page(id: 3, now: now)\n                    )\n                }\n\n                Directory(name: \"redirects\") {\n                    RawContentBundle(\n                        name: \"home-old\",\n                        rawContent: Mocks.RawContents.redirectHome(now: now)\n                    )\n                    RawContentBundle(\n                        name: \"about-old\",\n                        rawContent: Mocks.RawContents.redirectAbout(now: now)\n                    )\n                }\n\n                RawContentBundle(\n                    name: \"sitemap.xml\",\n                    rawContent: Mocks.RawContents.sitemapXML(now: now)\n                )\n\n                RawContentBundle(\n                    name: \"rss.xml\",\n                    rawContent: Mocks.RawContents.rssXML(now: now)\n                )\n\n                Directory(name: \"blog\") {\n                    Directory(name: \"posts\") {\n                        RawContentBundle(\n                            name: \"post-1\",\n                            rawContent: Mocks.RawContents.post(\n                                id: 1,\n                                now: now,\n                                // near past\n                                publication: formatter.string(\n                                    from: now.addingTimeInterval(-86400),\n                                    using: publicationConfig\n                                ),\n                                // near future\n                                expiration: formatter.string(\n                                    from: now.addingTimeInterval(86400),\n                                    using: expirationConfig\n                                ),\n                                featured: false,\n                                authorIDs: [1, 2],\n                                tagIDs: [1, 2]\n                            ),\n                            modificationDate: now\n                        )\n                        RawContentBundle(\n                            name: \"post-2\",\n                            rawContent: Mocks.RawContents.post(\n                                id: 2,\n                                now: now,\n                                // past\n                                publication: formatter.string(\n                                    from: now.addingTimeInterval(\n                                        -86400 * 2\n                                    ),\n                                    using: publicationConfig\n                                ),\n                                // future\n                                expiration: formatter.string(\n                                    from: now.addingTimeInterval(\n                                        86400 * 2\n                                    ),\n                                    using: expirationConfig\n                                ),\n                                featured: true,\n                                authorIDs: [1, 2, 3],\n                                tagIDs: [2]\n                            ),\n                            modificationDate: now\n                        )\n                        RawContentBundle(\n                            name: \"post-3\",\n                            rawContent: Mocks.RawContents.post(\n                                id: 3,\n                                now: now,\n                                // distant past\n                                publication: formatter.string(\n                                    from: now.addingTimeInterval(\n                                        -86400 * 3\n                                    ),\n                                    using: publicationConfig\n                                ),\n                                // distant future\n                                expiration: formatter.string(\n                                    from: now.addingTimeInterval(\n                                        86400 * 3\n                                    ),\n                                    using: expirationConfig\n                                ),\n                                featured: false,\n                                authorIDs: [2, 3],\n                                tagIDs: [2, 3]\n                            ),\n                            modificationDate: now\n                        )\n                        Directory(name: \"pages\") {\n                            RawContentBundle(\n                                name: \"{{post.pagination}}\",\n                                rawContent: Mocks.RawContents.postPagination(\n                                    now: now\n                                )\n                            )\n                        }\n                    }\n                    Directory(name: \"authors\") {\n                        RawContentBundle(\n                            name: \"author-1\",\n                            rawContent: Mocks.RawContents.author(\n                                id: 1,\n                                age: 18,\n                                now: now\n                            )\n                        )\n                        RawContentBundle(\n                            name: \"author-2\",\n                            rawContent: Mocks.RawContents.author(\n                                id: 2,\n                                age: 21,\n                                now: now\n                            )\n                        )\n                        RawContentBundle(\n                            name: \"author-3\",\n                            rawContent: Mocks.RawContents.author(\n                                id: 3,\n                                age: 42,\n                                now: now\n                            )\n                        )\n                    }\n                    Directory(name: \"tags\") {\n                        RawContentBundle(\n                            name: \"tag-1\",\n                            rawContent: Mocks.RawContents.tag(id: 1, now: now)\n                        )\n                        RawContentBundle(\n                            name: \"tag-2\",\n                            rawContent: Mocks.RawContents.tag(id: 2, now: now)\n                        )\n                        RawContentBundle(\n                            name: \"tag-3\",\n                            rawContent: Mocks.RawContents.tag(id: 3, now: now)\n                        )\n                    }\n                }\n                Directory(name: \"docs\") {\n                    Directory(name: \"categories\") {\n                        RawContentBundle(\n                            name: \"category-1\",\n                            rawContent: Mocks.RawContents.category(\n                                id: 1,\n                                now: now\n                            )\n                        )\n                        RawContentBundle(\n                            name: \"category-2\",\n                            rawContent: Mocks.RawContents.category(\n                                id: 2,\n                                now: now\n                            )\n                        )\n                        RawContentBundle(\n                            name: \"category-3\",\n                            rawContent: Mocks.RawContents.category(\n                                id: 3,\n                                now: now\n                            )\n                        )\n                    }\n                    Directory(name: \"guides\") {\n                        RawContentBundle(\n                            name: \"guide-1\",\n                            rawContent: Mocks.RawContents.guide(\n                                id: 1,\n                                categoryID: 1,\n                                now: now\n                            )\n                        )\n                        RawContentBundle(\n                            name: \"guide-2\",\n                            rawContent: Mocks.RawContents.guide(\n                                id: 2,\n                                categoryID: 1,\n                                now: now\n                            )\n                        )\n                        RawContentBundle(\n                            name: \"guide-3\",\n                            rawContent: Mocks.RawContents.guide(\n                                id: 3,\n                                categoryID: 1,\n                                now: now\n                            )\n                        )\n                        RawContentBundle(\n                            name: \"guide-4\",\n                            rawContent: Mocks.RawContents.guide(\n                                id: 4,\n                                categoryID: 2,\n                                now: now\n                            )\n                        )\n                        RawContentBundle(\n                            name: \"guide-5\",\n                            rawContent: Mocks.RawContents.guide(\n                                id: 5,\n                                categoryID: 2,\n                                now: now\n                            )\n                        )\n                        RawContentBundle(\n                            name: \"guide-6\",\n                            rawContent: Mocks.RawContents.guide(\n                                id: 6,\n                                categoryID: 2,\n                                now: now\n                            )\n                        )\n                        RawContentBundle(\n                            name: \"guide-7\",\n                            rawContent: Mocks.RawContents.guide(\n                                id: 7,\n                                categoryID: 3,\n                                now: now\n                            )\n                        )\n                        RawContentBundle(\n                            name: \"guide-8\",\n                            rawContent: Mocks.RawContents.guide(\n                                id: 8,\n                                categoryID: 3,\n                                now: now\n                            )\n                        )\n                        RawContentBundle(\n                            name: \"guide-9\",\n                            rawContent: Mocks.RawContents.guide(\n                                id: 9,\n                                categoryID: 3,\n                                now: now\n                            )\n                        )\n                    }\n                }\n            }\n            Mocks.E2E.types(postType: postType)\n            Mocks.E2E.pipelines()\n            Mocks.E2E.blocks()\n            Mocks.E2E.templates(debugContext: debugContext)\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSDKTests/Mocks/Mocks+Files.swift",
    "content": "//\n//  Mocks+Files.swift\n//  Toucan\n//\n//  Created by gerp83 on 2025. 04. 15..\n//\n\nimport FileManagerKitBuilder\nimport Foundation\n\nextension File {\n    enum Mocks {}\n}\n\nextension File.Mocks {\n    // MARK: -\n\n    static func replaceTransformer() -> File {\n        .init(\n            name: \"replace\",\n            attributes: [.posixPermissions: 0o777],\n            string: \"\"\"\n                #!/bin/bash\n                # Replaces all colons `:` with dashes `-` in the given file.\n                # Usage: replace-char --file <path>\n                UNKNOWN_ARGS=()\n                while [[ $# -gt 0 ]]; do\n                    case $1 in\n                        --file)\n                            TOUCAN_FILE=\"$2\"\n                            shift\n                            shift\n                            ;;\n                        -*|--*)\n                            UNKNOWN_ARGS+=(\"$1\" \"$2\")\n                            shift\n                            shift\n                            ;;\n                        *)\n                            shift\n                            ;;\n                    esac\n                done\n                if [[ -z \"${TOUCAN_FILE}\" ]]; then\n                    echo \"❌ No file specified with --file.\"\n                    exit 1\n                fi\n                echo \"📄 Processing file: ${TOUCAN_FILE}\"\n                if [[ ${#UNKNOWN_ARGS[@]} -gt 0 ]]; then\n                    echo \"ℹ️ Ignored unknown options: ${UNKNOWN_ARGS[*]}\"\n                fi\n                sed 's/:/-/g' \"${TOUCAN_FILE}\" > \"${TOUCAN_FILE}.tmp\" && mv \"${TOUCAN_FILE}.tmp\" \"${TOUCAN_FILE}\"\n                echo \"✅ Done replacing characters.\"\n                \"\"\"\n        )\n    }\n\n    // MARK: -\n\n    static func templateCSS() -> File {\n        File(\n            name: \"template.css\",\n            string: \"\"\"\n                header, footer, .page {\n                    max-width: 800px;\n                    margin: 0 auto;\n                }\n                header {\n                    text-align: center;\n                    border-bottom: 1px dotted black;\n                    padding-bottom: 16px;\n                }\n                footer {\n                    text-align: center;\n                    border-top: 1px dotted black;\n                    padding-top: 16px;\n                }\n                .page {\n                    padding-top: 16px;\n                    padding-bottom: 16px;\n                }\n                header #logo img {\n                    width: 64px;\n                }\n                \"\"\"\n        )\n    }\n\n    // MARK: -\n\n    static func template404View() -> MustacheFile {\n        .init(\n            name: \"404\",\n            contents: Mocks.Views.notFound()\n        )\n    }\n\n    static func templateDefaultView() -> MustacheFile {\n        .init(\n            name: \"default\",\n            contents: Mocks.Views.page()\n        )\n    }\n\n    static func templateHomeView() -> MustacheFile {\n        .init(\n            name: \"home\",\n            contents: Mocks.Views.home()\n        )\n    }\n\n    static func templateFooterView() -> MustacheFile {\n        .init(\n            name: \"footer\",\n            contents: \"\"\"\n                <footer>\n                    <p>This site was generated using <a href=\"https://www.swift.org/\" target=\"_blank\">Swift</a> & <a href=\"https://github.com/toucansites/toucan\" target=\"_blank\">Toucan</a>.</p>\n\n                    <p class=\"small\">{{site.title}} &copy; {{site.generation.formats.year}}.</p>\n                </footer>\n                \"\"\"\n        )\n    }\n\n    static func templateHeaderView() -> MustacheFile {\n        .init(\n            name: \"header\",\n            contents: \"\"\"\n                <header>\n                    <a id=\"logo\" href=\"/\">\n                        <img\n                            src=\"{{site.baseUrl}}/images/logo.png\"\n                            alt=\"Logo of {{site.title}}\"\n                            title=\"{{site.title}}\"\n                        >\n                    </a>\n                    <nav>\n                        <div class=\"navigation\">\n                            {{#site.navigation}}\n                            <a href=\"{{url}}\"{{#class}} class=\"{{.}}\"{{/class}}>{{label}}</a>\n                            {{/site.navigation}}\n                        </div>\n                    </nav>\n                </header>\n                \"\"\"\n        )\n    }\n\n    static func templateHTMLView() -> MustacheFile {\n        .init(\n            name: \"html\",\n            contents: Mocks.Views.html()\n        )\n    }\n\n    static func templateRedirectView() -> MustacheFile {\n        .init(\n            name: \"redirect\",\n            contents: Mocks.Views.redirect()\n        )\n    }\n\n    static func templateRSSView() -> MustacheFile {\n        .init(\n            name: \"rss\",\n            contents: Mocks.Views.rss()\n        )\n    }\n\n    static func templateSitemapView() -> MustacheFile {\n        .init(\n            name: \"sitemap\",\n            contents: Mocks.Views.sitemap()\n        )\n    }\n\n    // MARK: -\n\n    static func notFoundPage() -> RawContentBundle {\n        .init(\n            name: \"404\",\n            rawContent: Mocks.RawContents.notFoundPage()\n        )\n    }\n\n    static func aboutPage() -> RawContentBundle {\n        .init(\n            name: \"about\",\n            rawContent: Mocks.RawContents.aboutPage()\n        )\n    }\n\n    static func aboutPageStyleCSS() -> File {\n        File(\n            name: \"style.css\",\n            string: \"\"\"\n                #home h1 {\n                    text-transform: uppercase;\n                }\n                \"\"\"\n        )\n    }\n\n    static func homePage() -> MarkdownFile {\n        .init(\n            name: \"index\",\n            markdown: Mocks.RawContents.homePage().markdown\n        )\n    }\n\n    static func post(\n        id: Int,\n        now: Date = .init(),\n        publication: String,\n        expiration: String,\n        draft: Bool,\n        featured: Bool,\n        authorIDs: [Int],\n        tagIDs: [Int]\n    ) -> RawContentBundle {\n        .init(\n            name: \"post-\\(id)\",\n            rawContent: Mocks.RawContents.post(\n                id: id,\n                now: now,\n                publication: publication,\n                expiration: expiration,\n                draft: draft,\n                featured: featured,\n                authorIDs: authorIDs,\n                tagIDs: tagIDs\n            )\n        )\n    }\n\n    static func rssBundle() -> Directory {\n        Directory(name: \"rss.xml\") {\n            File(\n                name: \"index.yml\",\n                string: \"\"\"\n                    type: rss\n                    \"\"\"\n            )\n        }\n    }\n\n    static func sitemapBundle() -> Directory {\n        Directory(name: \"sitemap.xml\") {\n            File(\n                name: \"index.yml\",\n                string: \"\"\"\n                    type: sitemap\n                    \"\"\"\n            )\n        }\n    }\n\n    // MARK: - misc\n\n    static func svg1() -> File {\n        File(\n            name: \"test1.svg\",\n            string: \"\"\"\n                <svg width=\"800px\" height=\"800px\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n                <path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M6.46967 10.0303C6.17678 9.73744 6.17678 9.26256 6.46967 8.96967L11.4697 3.96967C11.7626 3.67678 12.2374 3.67678 12.5303 3.96967L17.5303 8.96967C17.8232 9.26256 17.8232 9.73744 17.5303 10.0303C17.2374 10.3232 16.7626 10.3232 16.4697 10.0303L12.75 6.31066L12.75 14.5C12.75 15.2133 12.9702 16.3 13.6087 17.1868C14.2196 18.0353 15.2444 18.75 17 18.75C17.4142 18.75 17.75 19.0858 17.75 19.5C17.75 19.9142 17.4142 20.25 17 20.25C14.7556 20.25 13.2804 19.298 12.3913 18.0632C11.5298 16.8667 11.25 15.4534 11.25 14.5L11.25 6.31066L7.53033 10.0303C7.23744 10.3232 6.76256 10.3232 6.46967 10.0303Z\" fill=\"#1C274C\"/>\n                </svg>\n                \"\"\"\n        )\n    }\n\n    static func svg2() -> File {\n        File(\n            name: \"test2.svg\",\n            string: \"\"\"\n                <svg width=\"800px\" height=\"800px\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n                <path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M6.46967 10.0303C6.17678 9.73744 6.17678 9.26256 6.46967 8.96967L11.4697 3.96967C11.7626 3.67678 12.2374 3.67678 12.5303 3.96967L17.5303 8.96967C17.8232 9.26256 17.8232 9.73744 17.5303 10.0303C17.2374 10.3232 16.7626 10.3232 16.4697 10.0303L12.75 6.31066L12.75 14.5C12.75 15.2133 12.9702 16.3 13.6087 17.1868C14.2196 18.0353 15.2444 18.75 17 18.75C17.4142 18.75 17.75 19.0858 17.75 19.5C17.75 19.9142 17.4142 20.25 17 20.25C14.7556 20.25 13.2804 19.298 12.3913 18.0632C11.5298 16.8667 11.25 15.4534 11.25 14.5L11.25 6.31066L7.53033 10.0303C7.23744 10.3232 6.76256 10.3232 6.46967 10.0303Z\" fill=\"#1C274C\"/>\n                </svg>\n                \"\"\"\n        )\n    }\n\n    static func yaml1() -> File {\n        File(\n            name: \"test1.yaml\",\n            string: \"\"\"\n                key1: value1\n                key2: value2\n                \"\"\"\n        )\n    }\n\n    static func yaml2() -> File {\n        File(\n            name: \"test2.yaml\",\n            string: \"\"\"\n                key3: value3\n                key4: value4\n                \"\"\"\n        )\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSDKTests/Mocks/Mocks+Pipelines.swift",
    "content": "//\n//  Mocks+Pipelines.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 21..\n//\n\nimport ToucanSource\nimport ToucanSDK\n\nextension Mocks.Pipelines {\n    static func html() -> Pipeline {\n        .init(\n            id: \"html\",\n            definesType: false,\n            scopes: [:],\n            queries: [\n                \"featured\": .init(\n                    contentType: \"post\",\n                    scope: Pipeline.Scope.Keys.list.rawValue,\n                    filter: .field(\n                        key: \"featured\",\n                        operator: .equals,\n                        value: true\n                    )\n                )\n            ],\n            dataTypes: .init(\n                date: .init(\n                    output: .defaults,\n                    formats: [\n                        \"rss\": .init(\n                            localization: .defaults,\n                            format: \"EEE, dd MMM yyyy HH:mm:ss Z\"\n                        ),\n                        \"sitemap\": .init(\n                            localization: .defaults,\n                            format: \"yyyy-MM-dd\"\n                        ),\n                        \"year\": .init(\n                            localization: .defaults,\n                            format: \"y\"\n                        ),\n                    ]\n                )\n            ),\n            contentTypes: .init(\n                include: [],\n                exclude: [\n                    \"rss\",\n                    \"sitemap\",\n                    \"redirect\",\n                    \"not-found\",\n                ],\n                lastUpdate: [\n                    \"page\",\n                    \"author\",\n                    \"tag\",\n                    \"post\",\n                    \"guide\",\n                    \"category\",\n                ],\n                filterRules: [\n                    \"*\": .field(\n                        key: \"draft\",\n                        operator: .equals,\n                        value: false\n                    ),\n                    \"post\": .and(\n                        [\n                            .field(\n                                key: \"draft\",\n                                operator: .equals,\n                                value: false\n                            ),\n                            .field(\n                                key: \"publication\",\n                                operator: .lessThanOrEquals,\n                                value: \"{{date.now}}\"\n                            ),\n                            .field(\n                                key: \"expiration\",\n                                operator: .greaterThanOrEquals,\n                                value: \"{{date.now}}\"\n                            ),\n                        ]\n                    ),\n                ]\n            ),\n            iterators: [\n                \"post.pagination\": .init(\n                    contentType: \"post\",\n                    limit: 2\n                )\n            ],\n            assets: .init(\n                behaviors: [\n                    .init(\n                        id: \"copy\",\n                        input: .init(name: \"*\", ext: \"*\"),\n                        output: .init(name: \"*\", ext: \"*\")\n                    )\n                ],\n                properties: [\n                    .init(\n                        action: .add,\n                        property: \"css\",\n                        resolvePath: true,\n                        input: .init(name: \"style\", ext: \"css\")\n                    ),\n                    .init(\n                        action: .add,\n                        property: \"js\",\n                        resolvePath: false,\n                        input: .init(name: \"main\", ext: \"js\")\n                    ),\n                    .init(\n                        action: .set,\n                        property: \"image\",\n                        resolvePath: true,\n                        input: .init(name: \"cover\", ext: \"jpg\")\n                    ),\n                    .init(\n                        action: .load,\n                        property: \"svg\",\n                        resolvePath: false,\n                        input: .init(\n                            name: \"icon\",\n                            ext: \"svg\"\n                        )\n                    ),\n                    .init(\n                        action: .load,\n                        property: \"svgs\",\n                        resolvePath: true,\n                        input: .init(\n                            path: \"icons\",\n                            name: \"*\",\n                            ext: \"svg\"\n                        )\n                    ),\n                    .init(\n                        action: .parse,\n                        property: \"yaml\",\n                        resolvePath: false,\n                        input: .init(\n                            name: \"data\",\n                            ext: \"yml\"\n                        )\n                    ),\n                    .init(\n                        action: .parse,\n                        property: \"yamls\",\n                        resolvePath: true,\n                        input: .init(\n                            path: \"dataset\",\n                            name: \"*\",\n                            ext: \"yaml\"\n                        )\n                    ),\n                ]\n            ),\n            transformers: [:],\n            engine: .init(\n                id: \"mustache\",\n                options: [\n                    \"contentTypes\": [\n                        \"page\": [\n                            ViewFrontMatterKeys.view.rawValue: \"pages.default\"\n                        ],\n                        \"post\": [\n                            ViewFrontMatterKeys.view.rawValue:\n                                \"blog.post.default\"\n                        ],\n                        \"author\": [\n                            ViewFrontMatterKeys.view.rawValue:\n                                \"blog.author.default\"\n                        ],\n                        \"tag\": [\n                            ViewFrontMatterKeys.view.rawValue:\n                                \"blog.tag.default\"\n                        ],\n                        \"category\": [\n                            ViewFrontMatterKeys.view.rawValue:\n                                \"docs.category.default\"\n                        ],\n                        \"guide\": [\n                            ViewFrontMatterKeys.view.rawValue:\n                                \"docs.guide.default\"\n                        ],\n                    ]\n                ]\n            ),\n            output: .init(\n                path: \"{{slug}}\",\n                file: \"index\",\n                ext: \"html\"\n            )\n        )\n    }\n\n    static func notFound() -> Pipeline {\n        .init(\n            id: \"not-found\",\n            definesType: true,\n            scopes: [:],\n            queries: [:],\n            dataTypes: .defaults,\n            contentTypes: .init(\n                include: [\n                    \"not-found\"\n                ],\n                exclude: [],\n                lastUpdate: [],\n                filterRules: [:]\n            ),\n            iterators: [:],\n            assets: .defaults,\n            transformers: [:],\n            engine: .init(\n                id: \"mustache\",\n                options: [\n                    \"contentTypes\": [\n                        \"not-found\": [\n                            ViewFrontMatterKeys.view.rawValue: \"pages.404\"\n                        ]\n                    ]\n                ]\n            ),\n            output: .init(\n                path: \"\",\n                file: \"404\",\n                ext: \"html\"\n            )\n        )\n    }\n\n    static func redirect() -> Pipeline {\n        .init(\n            id: \"redirect\",\n            definesType: true,\n            scopes: [:],\n            queries: [:],\n            dataTypes: .defaults,\n            contentTypes: .init(\n                include: [\n                    \"redirect\"\n                ],\n                exclude: [],\n                lastUpdate: [],\n                filterRules: [:]\n            ),\n            iterators: [:],\n            assets: .defaults,\n            transformers: [:],\n            engine: .init(\n                id: \"mustache\",\n                options: [\n                    \"contentTypes\": [\n                        \"redirect\": [\n                            ViewFrontMatterKeys.view.rawValue: \"redirect\"\n                        ]\n                    ]\n                ]\n            ),\n            output: .init(\n                path: \"{{slug}}\",\n                file: \"index\",\n                ext: \"html\"\n            )\n        )\n    }\n\n    static func rss() -> Pipeline {\n        .init(\n            id: \"rss\",\n            definesType: true,\n            scopes: [:],\n            queries: [\n                \"posts\": .init(\n                    contentType: \"post\",\n                    scope: Pipeline.Scope.Keys.list.rawValue,\n                    orderBy: [\n                        .init(\n                            key: SystemPropertyKeys.lastUpdate.rawValue,\n                            direction: .desc\n                        )\n                    ]\n                )\n            ],\n            dataTypes: .init(\n                date: .init(\n                    output: .defaults,\n                    formats: [\n                        \"rss\": .init(\n                            localization: .defaults,\n                            format: \"EEE, dd MMM yyyy HH:mm:ss Z\"\n                        )\n                    ]\n                )\n            ),\n            contentTypes: .init(\n                include: [\n                    \"rss\"\n                ],\n                exclude: [],\n                lastUpdate: [\n                    \"post\"\n                ],\n                filterRules: [:]\n            ),\n            iterators: [:],\n            assets: .defaults,\n            transformers: [:],\n            engine: .init(\n                id: \"mustache\",\n                options: [\n                    \"contentTypes\": [\n                        \"rss\": [\n                            ViewFrontMatterKeys.view.rawValue: \"rss\"\n                        ]\n                    ]\n                ]\n            ),\n            output: .init(\n                path: \"\",\n                file: \"rss\",\n                ext: \"xml\"\n            )\n        )\n    }\n\n    static func sitemap() -> Pipeline {\n        .init(\n            id: \"sitemap\",\n            definesType: true,\n            scopes: [:],\n            queries: [\n                \"pages\": .init(\n                    contentType: \"page\",\n                    scope: Pipeline.Scope.Keys.list.rawValue,\n                    orderBy: [\n                        .init(\n                            key: SystemPropertyKeys.lastUpdate.rawValue,\n                            direction: .desc\n                        ),\n                        .init(key: \"id\", direction: .desc),\n                    ]\n                ),\n                \"posts\": .init(\n                    contentType: \"post\",\n                    scope: Pipeline.Scope.Keys.list.rawValue,\n                    orderBy: [\n                        .init(\n                            key: SystemPropertyKeys.lastUpdate.rawValue,\n                            direction: .desc\n                        )\n                    ]\n                ),\n                \"authors\": .init(\n                    contentType: \"author\",\n                    scope: Pipeline.Scope.Keys.list.rawValue,\n                    orderBy: [\n                        .init(\n                            key: SystemPropertyKeys.lastUpdate.rawValue,\n                            direction: .desc\n                        )\n                    ]\n                ),\n                \"tags\": .init(\n                    contentType: \"tag\",\n                    scope: Pipeline.Scope.Keys.list.rawValue,\n                    orderBy: [\n                        .init(\n                            key: SystemPropertyKeys.lastUpdate.rawValue,\n                            direction: .desc\n                        )\n                    ]\n                ),\n            ],\n            dataTypes: .init(\n                date: .init(\n                    output: .defaults,\n                    formats: [\n                        \"sitemap\": .init(\n                            localization: .defaults,\n                            format: \"yyyy-MM-dd\"\n                        )\n                    ]\n                )\n            ),\n            contentTypes: .init(\n                include: [\n                    \"sitemap\"\n                ],\n                exclude: [],\n                lastUpdate: [],\n                filterRules: [:]\n            ),\n            iterators: [\n                \"post.pagination\": .init(\n                    contentType: \"post\",\n                    limit: 2\n                )\n            ],\n            assets: .init(\n                behaviors: [],\n                properties: []\n            ),\n            transformers: [:],\n            engine: .init(\n                id: \"mustache\",\n                options: [\n                    \"contentTypes\": [\n                        \"sitemap\": [\n                            ViewFrontMatterKeys.view.rawValue: \"sitemap\"\n                        ]\n                    ]\n                ]\n            ),\n            output: .init(\n                path: \"\",\n                file: \"sitemap\",\n                ext: \"xml\"\n            )\n        )\n    }\n\n    static func api() -> Pipeline {\n        .init(\n            id: \"api\",\n            definesType: true,\n            scopes: [:],\n            queries: [\n                \"posts\": .init(\n                    contentType: \"post\",\n                    scope: Pipeline.Scope.Keys.list.rawValue,\n                    orderBy: [\n                        .init(\n                            key: \"publication\",\n                            direction: .desc\n                        )\n                    ]\n                )\n            ],\n            dataTypes: .defaults,\n            contentTypes: .init(\n                include: [\"api\"],\n                exclude: [],\n                lastUpdate: [],\n                filterRules: [:]\n            ),\n            iterators: [\n                \"post.pagination\": .init(\n                    contentType: \"post\",\n                    limit: 2\n                )\n            ],\n            assets: .defaults,\n            transformers: [:],\n            engine: .init(\n                id: \"json\",\n                options: [\n                    \"keyPath\": \"context.posts\"\n                ]\n            ),\n            output: .init(\n                path: \"api\",\n                file: \"posts\",\n                ext: \"json\"\n            )\n        )\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSDKTests/Mocks/Mocks+RawContents.swift",
    "content": "//\n//  Mocks+RawContents.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 21..\n//\n\nimport Foundation\nimport ToucanSource\nimport ToucanSDK\n\nextension Mocks.RawContents {\n    static func homePage(\n        now: Date = .init()\n    ) -> RawContent {\n        .init(\n            origin: .init(\n                path: .init(\"\"),\n                slug: \"\"\n            ),\n            markdown: .init(\n                frontMatter: [\n                    \"title\": \"Home page\",\n                    \"description\": \"Home page description\",\n                ],\n                contents: \"\"\"\n                    # Home page\n\n                    Home page contents\n                    \"\"\"\n            ),\n            lastModificationDate: now.timeIntervalSince1970,\n            assetsPath: \"assets\",\n            assets: []\n        )\n    }\n\n    static func notFoundPage(\n        now: Date = .init()\n    ) -> RawContent {\n        .init(\n            origin: .init(\n                path: .init(\"404\"),\n                slug: \"404\"\n            ),\n            markdown: .init(\n                frontMatter: [\n                    \"type\": \"not-found\",\n                    \"title\": \"Not found page\",\n                    \"description\": \"Not found page description\",\n                ],\n                contents: \"\"\"\n                    # Not found\n\n                    Not found page contents\n                    \"\"\"\n            ),\n            lastModificationDate: now.timeIntervalSince1970,\n            assetsPath: \"assets\",\n            assets: []\n        )\n    }\n\n    static func aboutPage(\n        now: Date = .init()\n    ) -> RawContent {\n        .init(\n            origin: .init(\n                path: .init(\"about\"),\n                slug: \"about\"\n            ),\n            markdown: .init(\n                frontMatter: [\n                    \"title\": \"About page\",\n                    \"description\": \"About page description\",\n                    \"css\": [\n                        \"/assets/about/about.css\",\n                        \"https://unpkg.com/test@1.0.0.css\",\n                    ],\n                ],\n                contents: \"\"\"\n                    # About page\n\n                    About page contents\n                    \"\"\"\n            ),\n            lastModificationDate: now.timeIntervalSince1970,\n            assetsPath: \"assets\",\n            assets: [\n                \"style.css\",\n                \"main.js\",\n            ]\n        )\n    }\n\n    static func contextPage(\n        now: Date = .init()\n    ) -> RawContent {\n        .init(\n            origin: .init(\n                path: .init(\"context\"),\n                slug: \"context\"\n            ),\n            markdown: .init(\n                frontMatter: [\n                    \"title\": \"Context page\",\n                    \"description\": \"Context page description\",\n\n                    ViewFrontMatterKeys.views.rawValue: [\n                        \"*\": \"pages.context\",\n                        \"invalid-pipeline\": \"invalid-view\",\n                    ],\n                ],\n                contents: \"\"\"\n                    # Context page\n\n                    Context page contents\n                    \"\"\"\n            ),\n            lastModificationDate: now.timeIntervalSince1970,\n            assetsPath: \"assets\",\n            assets: []\n        )\n    }\n\n    static func redirectHome(\n        now: Date = .init()\n    ) -> RawContent {\n        .init(\n            origin: .init(\n                path: .init(\"redirects/home-old\"),\n                slug: \"home-old\"\n            ),\n            markdown: .init(\n                frontMatter: [\n                    \"type\": \"redirect\",\n                    \"to\": \"\",\n                    \"code\": \"301\",\n                ],\n                contents: \"\"\n            ),\n            lastModificationDate: now.timeIntervalSince1970,\n            assetsPath: \"assets\",\n            assets: []\n        )\n    }\n\n    static func redirectAbout(\n        now: Date = .init()\n    ) -> RawContent {\n        .init(\n            origin: .init(\n                path: .init(\"redirects/about-old\"),\n                slug: \"about-old\"\n            ),\n            markdown: .init(\n                frontMatter: [\n                    \"type\": \"redirect\",\n                    \"to\": \"about\",\n                    \"code\": \"301\",\n                ],\n                contents: \"\"\n            ),\n            lastModificationDate: now.timeIntervalSince1970,\n            assetsPath: \"assets\",\n            assets: []\n        )\n    }\n\n    static func rssXML(\n        now: Date = .init()\n    ) -> RawContent {\n        .init(\n            origin: .init(\n                path: .init(\"rss.xml\"),\n                slug: \"rss.xml\"\n            ),\n            markdown: .init(\n                frontMatter: [\n                    \"type\": \"rss\"\n                ],\n                contents: \"\"\n            ),\n            lastModificationDate: now.timeIntervalSince1970,\n            assetsPath: \"assets\",\n            assets: []\n        )\n    }\n\n    static func sitemapXML(\n        now: Date = .init()\n    ) -> RawContent {\n        .init(\n            origin: .init(\n                path: .init(\"sitemap.xml\"),\n                slug: \"sitemap.xml\"\n            ),\n            markdown: .init(\n                frontMatter: [\n                    \"type\": \"sitemap\"\n                ],\n                contents: \"\"\n            ),\n            lastModificationDate: now.timeIntervalSince1970,\n            assetsPath: \"assets\",\n            assets: []\n        )\n    }\n\n    static func page(\n        id: Int,\n        now: Date = .init()\n    ) -> RawContent {\n        .init(\n            origin: .init(\n                path: .init(\"pages/page-\\(id)\"),\n                slug: \"pages/page-\\(id)\"\n            ),\n            markdown: .init(\n                frontMatter: [\n                    \"title\": \"Page #\\(id)\",\n                    \"description\": \"Page #\\(id) description\",\n                ],\n                contents: \"\"\"\n                    # Page #\\(id)\n\n                    Page #\\(id) contents\n                    \"\"\"\n            ),\n            lastModificationDate: now.timeIntervalSince1970,\n            assetsPath: \"assets\",\n            assets: []\n        )\n    }\n\n    static func author(\n        id: Int,\n        age: Int = 21,\n        now: Date = .init()\n    ) -> RawContent {\n        .init(\n            origin: .init(\n                path: .init(\"blog/authors/author-\\(id)\"),\n                slug: \"blog/authors/author-\\(id)\"\n            ),\n            markdown: .init(\n                frontMatter: [\n                    \"name\": \"Author #\\(id)\",\n                    \"description\": \"Author #\\(id) description\",\n                    \"image\": \"./assets/author-\\(id).jpg\",\n                    \"age\": .init(age),\n                ],\n\n                contents: \"\"\"\n                    # Author #\\(id)\n\n                    Author page contents\n                    \"\"\"\n            ),\n            lastModificationDate: now.timeIntervalSince1970,\n            assetsPath: \"assets\",\n            assets: [\n                \"author-\\(id).jpg\"\n            ]\n        )\n    }\n\n    static func tag(\n        id: Int,\n        now: Date = .init()\n    ) -> RawContent {\n        .init(\n            origin: .init(\n                path: .init(\"blog/tags/tag-\\(id)\"),\n                slug: \"blog/tags/tag-\\(id)\"\n            ),\n            markdown: .init(\n                frontMatter: [\n                    \"title\": \"Tag \\(id)\",\n                    \"description\": \"Tag #\\(id) description\",\n                ],\n\n                contents: \"\"\"\n                    # Tag #\\(id)\n\n                    Tag page contents\n                    \"\"\"\n            ),\n            lastModificationDate: now.timeIntervalSince1970,\n            assetsPath: \"assets\",\n            assets: []\n        )\n    }\n\n    static func post(\n        id: Int,\n        now: Date = .init(),\n        publication: String,\n        expiration: String,\n        draft: Bool = false,\n        featured: Bool = false,\n        authorIDs: [Int] = [],\n        tagIDs: [Int] = []\n    ) -> RawContent {\n        .init(\n            origin: .init(\n                path: .init(\"blog/posts/post-\\(id)\"),\n                slug: \"blog/posts/post-\\(id)\"\n            ),\n            markdown: .init(\n                frontMatter: [\n                    \"title\": \"Post #\\(id)\",\n                    \"description\": \"Post #\\(id) description\",\n                    \"publication\": .init(publication),\n                    \"expiration\": .init(expiration),\n                    \"draft\": .init(draft),\n                    \"featured\": .init(featured),\n                    \"authors\": .init(authorIDs.map { \"author-\\($0)\" }),\n                    \"tags\": .init(tagIDs.map { \"tag-\\($0)\" }),\n                    \"rating\": .init(Double(id)),\n                    \"image\": \"cover-\\(id).jpg\",\n                ],\n\n                contents: \"\"\"\n                    # Post #\\(id)\n\n                    Post page contents\n                    \"\"\"\n            ),\n            lastModificationDate: now.timeIntervalSince1970,\n            assetsPath: \"assets\",\n            assets: [\n                \"cover.jpg\"\n            ]\n        )\n    }\n\n    static func postPagination(\n        now: Date = .init()\n    ) -> RawContent {\n        .init(\n            origin: .init(\n                path: .init(\"blog/posts/pages/{{post.pagination}}\"),\n                slug: \"blog/posts/pages/{{post.pagination}}\"\n            ),\n            markdown: .init(\n                frontMatter: [\n                    \"type\": \"page\",\n                    \"title\": \"Post pagination page {{number}} / {{total}}\",\n                    \"description\": \"Post pagination page description\",\n                ],\n\n                contents: \"\"\"\n                    # Post pagination page {{number}} / {{total}}\n\n                    Post pagination page contents\n                    \"\"\"\n            ),\n            lastModificationDate: now.timeIntervalSince1970,\n            assetsPath: \"assets\",\n            assets: []\n        )\n    }\n\n    static func category(\n        id: Int,\n        now: Date = .init()\n    ) -> RawContent {\n        .init(\n            origin: .init(\n                path: .init(\"docs/categories/category-\\(id)\"),\n                slug: \"docs/categories/category-\\(id)\"\n            ),\n            markdown: .init(\n                frontMatter: [\n                    \"title\": \"Category #\\(id)\",\n                    \"description\": \"Category #\\(id) description\",\n                    \"order\": .init(id),\n                ],\n\n                contents: \"\"\"\n                    # Category #\\(id)\n\n                    Category page contents\n                    \"\"\"\n            ),\n            lastModificationDate: now.timeIntervalSince1970,\n            assetsPath: \"assets\",\n            assets: []\n        )\n    }\n\n    static func guide(\n        id: Int,\n        categoryID: Int,\n        now: Date = .init()\n    ) -> RawContent {\n        .init(\n            origin: .init(\n                path: .init(\"docs/guides/guide-\\(id)\"),\n                slug: \"docs/guides/guide-\\(id)\"\n            ),\n            markdown: .init(\n                frontMatter: [\n                    \"title\": \"Guide #\\(id)\",\n                    \"description\": \"Guide #\\(id) description\",\n                    \"category\": \"category-\\(categoryID)\",\n                    \"order\": .init(id),\n                ],\n\n                contents: \"\"\"\n                    # Guide #\\(id)\n\n                    Guide page contents\n                    \"\"\"\n            ),\n            lastModificationDate: now.timeIntervalSince1970,\n            assetsPath: \"assets\",\n            assets: []\n        )\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSDKTests/Mocks/Mocks+Templates.swift",
    "content": "//\n//  Mocks+Templates.swift\n//  Toucan\n//\n//  Created by Viasz-Kádi Ferenc on 2025. 06. 17..\n//\n\nimport ToucanCore\n@testable import ToucanSource\n\nextension Mocks.Templates {\n\n    static func metadata(\n        generatorVersion: Template.Metadata.GeneratorVersion = .init(\n            value: GeneratorInfo.current.release,\n            type: .upNextMajor\n        )\n    ) -> Template.Metadata {\n        let url = \"http://localhost:8080/\"\n\n        return .init(\n            name: \"Test Template\",\n            description: \"Test Template description\",\n            url: url,\n            version: \"1.0.0\",\n            generatorVersion: generatorVersion,\n            license: .init(\n                name: \"Test License\",\n                url: url\n            ),\n            authors: [\n                .init(\n                    name: \"Test Template Author\",\n                    url: url\n                )\n            ],\n            demo: .init(\n                url: url\n            ),\n            tags: [\n                \"blog\",\n                \"adaptive-colors\",\n            ]\n        )\n    }\n\n    static func example(\n        generatorVersion: Template.Metadata.GeneratorVersion = .init(\n            value: GeneratorInfo.current.release,\n            type: .upNextMajor\n        )\n    ) -> Template {\n        .init(\n            metadata: Self.metadata(generatorVersion: generatorVersion),\n            components: .init(\n                assets: [\n                    \"css/theme.css\",\n                    \"css/variables.css\",\n                ],\n                views: [\n                    .init(\n                        id: \"pages.404\",\n                        path: \"pages/404.mustache\",\n                        contents: Mocks.Views.notFound()\n                    ),\n                    .init(\n                        id: \"blog.post.default\",\n                        path: \"blog/post/default.mustache\",\n                        contents: Mocks.Views.post()\n                    ),\n                    .init(\n                        id: \"blog.author.default\",\n                        path: \"blog/author/default.mustache\",\n                        contents: Mocks.Views.author()\n                    ),\n                    .init(\n                        id: \"html\",\n                        path: \"html.mustache\",\n                        contents: Mocks.Views.html()\n                    ),\n                ]\n            ),\n            overrides: .init(\n                assets: [],\n                views: []\n            ),\n            content: .init(\n                assets: [\n                    \"splash/750x1334.png\",\n                    \"splash/750x1334~dark.png\",\n                    \"icons/320.png\",\n                    \"CNAME\",\n                ],\n                views: []\n            )\n        )\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSDKTests/Mocks/Mocks+Views.swift",
    "content": "//\n//  Mocks+Views.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 21..\n//\n\n@testable import ToucanSource\n\nextension Mocks.Views {\n    static func all(\n        contextValue: String = \"{{.}}\"\n    ) -> Template {\n        .init(\n            metadata: .init(\n                name: \"Mock\",\n                description: \"Mock template\",\n                url: nil,\n                version: \"1.0.0-beta.6\",\n                generatorVersion: .init(\n                    value: .init(\"1.0.0-beta.6\")!,\n                    type: .exact\n                ),\n                license: nil,\n                authors: [],\n                demo: nil,\n                tags: []\n            ),\n            components: .init(\n                assets: [],\n                views: [\n                    .init(id: \"html\", path: \"\", contents: html()),\n                    .init(id: \"redirect\", path: \"\", contents: redirect()),\n                    .init(id: \"rss\", path: \"\", contents: rss()),\n                    .init(id: \"sitemap\", path: \"\", contents: sitemap()),\n\n                    .init(id: \"pages.default\", path: \"\", contents: page()),\n                    .init(id: \"pages.404\", path: \"\", contents: notFound()),\n                    .init(\n                        id: \"pages.context\",\n                        path: \"\",\n                        contents: context(value: contextValue)\n                    ),\n\n                    .init(\n                        id: \"docs.category.default\",\n                        path: \"\",\n                        contents: category()\n                    ),\n                    .init(\n                        id: \"docs.guide.default\",\n                        path: \"\",\n                        contents: guide()\n                    ),\n\n                    .init(id: \"blog.post.default\", path: \"\", contents: post()),\n                    .init(\n                        id: \"blog.author.default\",\n                        path: \"\",\n                        contents: author()\n                    ),\n                    .init(id: \"blog.tag.default\", path: \"\", contents: tag()),\n\n                    .init(\n                        id: \"partials.blog.author\",\n                        path: \"\",\n                        contents: partialAuthor()\n                    ),\n                    .init(\n                        id: \"partials.blog.tag\",\n                        path: \"\",\n                        contents: partialTag()\n                    ),\n                    .init(\n                        id: \"partials.blog.post\",\n                        path: \"\",\n                        contents: partialPost()\n                    ),\n\n                    .init(\n                        id: \"partials.docs.category\",\n                        path: \"\",\n                        contents: partialCategory()\n                    ),\n                    .init(\n                        id: \"partials.docs.guide\",\n                        path: \"\",\n                        contents: partialGuide()\n                    ),\n                ]\n            ),\n            overrides: .init(assets: [], views: []),\n            content: .init(assets: [], views: [])\n        )\n    }\n\n    static func redirect() -> String {\n        #\"\"\"\n        <!DOCTYPE html>\n        <html{{#site.language}} lang=\"{{.}}\"{{/site.language}}>\n          <meta charset=\"utf-8\">\n          <title>Redirecting&hellip;</title>\n          <link rel=\"canonical\" href=\"{{baseUrl}}/{{page.to}}\">\n          <script>location=\"{{baseUrl}}/{{page.to}}\"</script>\n          <meta http-equiv=\"refresh\" content=\"0; url={{baseUrl}}/{{page.to}}\">\n          <meta name=\"robots\" content=\"noindex\">\n          <h1>Redirecting&hellip;</h1>\n          <a href=\"{{baseUrl}}/{{page.to}}\">Click here if you are not redirected.</a>\n        </html>\n        \"\"\"#\n    }\n\n    static func rss() -> String {\n        #\"\"\"\n        <rss xmlns:atom=\"http://www.w3.org/2005/Atom\" version=\"2.0\">\n        <channel>\n            <title>{{site.name}}</title>\n            <description>{{site.description}}</description>\n            <link>{{baseUrl}}</link>\n            <language>{{site.language}}</language>\n            <lastBuildDate>{{generation.formats.rss}}</lastBuildDate>\n            <pubDate>{{lastUpdate.formats.rss}}</pubDate>\n            <ttl>250</ttl>\n            <atom:link href=\"{{baseUrl}}/rss.xml\" rel=\"self\" type=\"application/rss+xml\"/>\n\n            {{#context.posts}}\n            <item>\n                <guid isPermaLink=\"true\">{{permalink}}</guid>\n                <title><![CDATA[ {{title}} ]]></title>\n                <description><![CDATA[ {{description}} ]]></description>\n                <link>{{permalink}}</link>\n                <pubDate>{{publication.formats.rss}}</pubDate>\n            </item>\n            {{/context.posts}}\n\n        </channel>\n        </rss>\n        \"\"\"#\n    }\n\n    static func sitemap() -> String {\n        #\"\"\"\n        <urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n            <url>\n\n            {{#context.pages}}\n                <loc>{{permalink}}</loc>\n                <lastmod>{{lastUpdate.formats.sitemap}}</lastmod>\n            {{/context.pages}}\n\n            {{#context.posts}}\n                <loc>{{permalink}}</loc>\n                <lastmod>{{lastUpdate.formats.sitemap}}</lastmod>\n            {{/context.posts}}\n\n            {{#context.authors}}\n                <loc>{{permalink}}</loc>\n                <lastmod>{{lastUpdate.formats.sitemap}}</lastmod>\n            {{/context.authors}}\n\n            {{#context.tags}}\n                <loc>{{permalink}}</loc>\n                <lastmod>{{lastUpdate.formats.sitemap}}</lastmod>\n            {{/context.tags}}\n\n            {{#context.categories}}\n                <loc>{{permalink}}</loc>\n                <lastmod>{{lastUpdate.formats.sitemap}}</lastmod>\n            {{/context.categories}}\n\n            {{#context.guides}}\n                <loc>{{permalink}}</loc>\n                <lastmod>{{lastUpdate.formats.sitemap}}</lastmod>\n            {{/context.guides}}\n\n            </url>\n        </urlset>\n        \"\"\"#\n    }\n\n    static func html() -> String {\n        #\"\"\"\n        <!DOCTYPE html>\n        <html {{#site.locale}}lang=\"{{.}}\"{{/site.locale}}>\n        <head>\n            <meta charset=\"utf-8\">\n            {{#page.noindex}}<meta name=\"robots\" content=\"noindex\">{{/page.noindex}}\n            <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n            <meta name=\"description\" content=\"{{page.description}}\">\n\n            <title>{{page.title}}</title>\n\n            <link rel=\"canonical\" href=\"{{page.permalink}}\">\n            {{#page.hreflang}}\n            <link rel=\"alternate\" hreflang=\"{{lang}}\" href=\"{{url}}\">\n            {{/page.hreflang}}\n            {{#page.prev}}<link rel=\"prev\" href=\"{{permalink}}\">{{/page.prev}}\n            {{#page.next}}<link rel=\"next\" href=\"{{permalink}}\">{{/page.next}}\n\n            <link rel=\"stylesheet\" href=\"{{baseUrl}}/css/style.css\">\n            <link rel=\"stylesheet\" href=\"{{baseUrl}}/css/template.css\">\n\n            {{#page.css}}<link rel=\"stylesheet\" href=\"{{.}}\">{{/page.css}}\n        </head>\n\n        <body>\n            {{> partials.navigation}}\n\n            <main id=\"page-container\">\n            {{$main}}\n                <p>No content.</p>\n            {{/main}}\n            </main>\n\n            {{> partials.footer}}\n\n            {{#page.js}}<script src=\"{{.}}\" async></script>{{/page.js}}\n        </body>\n        </html>\n        \"\"\"#\n    }\n\n    static func notFound() -> String {\n        #\"\"\"\n        {{<html}}\n        {{$main}}\n\n        <div id=\"not-found\" class=\"wrapper\">\n            {{& page.contents.html}}\n        </div>\n\n        {{/main}}\n        {{/html}}\n        \"\"\"#\n    }\n\n    static func navigation() -> String {\n        #\"\"\"\n        <header id=\"navigation\">\n            <nav>\n                <div class=\"menu-items\">\n                    {{#site.navigation}}\n                    <a href=\"{{url}}\"{{#class}} class=\"{{.}}\"{{/class}}>{{label}}</a>\n                    {{/site.navigation}}\n                </div>\n            </nav>\n        </header>\n        \"\"\"#\n    }\n\n    static func footer() -> String {\n        #\"\"\"\n        <footer id=\"site-footer\">\n            <p>Toucan</p>\n        </footer>\n        \"\"\"#\n    }\n\n    static func page() -> String {\n        #\"\"\"\n        {{<html}}\n        {{$main}}\n\n        {{& page.contents.html}}\n\n        {{/main}}\n        {{/html}}\n        \"\"\"#\n    }\n\n    static func context(\n        value: String\n    ) -> String {\n        #\"\"\"\n        \\#(value)\n        \"\"\"#\n    }\n\n    static func post() -> String {\n        #\"\"\"\n        {{<html}}\n        {{$main}}\n        <article class=\"post\">\n\n            <header>\n                {{#page.image}}<img src=\"{{page.image}}\" alt=\"{{page.title}}\">{{/page.image}}\n                <div class=\"meta\">\n                    <time datetime=\"{{page.publication.formats.iso8601}}\">{{page.publication.date.short}} {{page.publication.time.short}}</time>\n                    {{#page.contents.readingTime}} &middot; <span class=\"reading-time\">{{.}} min read</span>{{/page.contents.readingTime}}\n                </div>\n                <h1>{{page.title}}</h1>\n                <hr>\n                <p class=\"excerpt\">{{page.description}}</p>\n\n\n            </header>\n\n            <section>\n\n            {{& page.contents.html}}\n\n            </section>\n\n            <footer class=\"grid grid-221\">\n                <div class=\"author-list\">\n                {{#page.authors}}\n                    <a href=\"{{permalink}}\">\n                    {{#image}}<img class=\"medium rounded\" src=\"{{image}}\" alt=\"{{title}}\">{{/image}}\n                    </a>\n                {{/page.authors}}\n                </div>\n                <div class=\"tag-list\">\n                {{#page.tags}}\n                    <a href=\"{{permalink}}\"><small>{{title}}</small></a>\n                {{/page.tags}}\n                </div>\n            </footer>\n\n            <section>\n            {{#empty(page.related)}}\n            {{/empty(page.related)}}\n            {{^empty(page.related)}}\n            <h4>Related articles</h4>\n            <br>\n            <div class=\"grid grid-221\">\n            {{#page.related}}\n                {{> partials.blog.post}}\n            {{/page.related}}\n            </div>\n            {{/empty(page.related)}}\n            </section>\n\n        </article>\n\n        <div class=\"fixed-toc\">\n        {{> partials.outline }}\n        </div>\n\n        {{/main}}\n        {{/html}}\n        \"\"\"#\n    }\n\n    static func posts() -> String {\n        #\"\"\"\n        {{<html}}\n        {{$main}}\n\n        {{& page.contents.html}}\n\n        <div id=\"posts\" class=\"wrapper\">\n\n            {{#empty(iterator.items)}}\n            Empty.\n            {{/empty(iterator.items)}}\n            {{^empty(iterator.items)}}\n            <div class=\"grid grid-321\">\n            {{#iterator.items}}\n                {{> partials.blog.post}}\n            {{/iterator.items}}\n            </div>\n            {{/empty(iterator.items)}}\n\n            {{#empty(iterator.links)}}\n            {{/empty(iterator.links)}}\n            {{^empty(iterator.links)}}\n            <div class=\"pagination\">\n            {{#iterator.links}}\n                {{> partials.pagination}}\n            {{/iterator.links}}\n            </div>\n            {{/empty(iterator.links)}}\n\n        </div>\n\n        {{/main}}\n        {{/html}}\n        \"\"\"#\n    }\n\n    static func tags() -> String {\n        #\"\"\"\n        {{<html}}\n        {{$main}}\n\n        {{& page.contents.html}}\n\n        <div id=\"tags\" class=\"wrapper\">\n            {{#empty(context.tags)}}\n            Empty.\n            {{/empty(context.tags)}}\n            {{^empty(context.tags)}}\n            <div class=\"grid grid-221\">\n            {{#context.tags}}\n                {{> partials.blog.tag}}\n            {{/context.tags}}\n\n            </div>\n            {{/empty(context.tags)}}\n        </div>\n\n        {{/main}}\n        {{/html}}    \n        \"\"\"#\n    }\n\n    static func authors() -> String {\n        #\"\"\"\n        {{<html}}\n        {{$main}}\n\n        {{& page.contents.html}}\n\n        <div id=\"authors\" class=\"wrapper\">\n\n            {{#empty(context.authors)}}\n            Empty.\n            {{/empty(context.authors)}}\n            {{^empty(context.authors)}}\n            <div class=\"grid grid-221\">\n            {{#context.authors}}\n                {{> partials.blog.author}}\n            {{/context.authors}}\n            </div>\n            {{/empty(context.authors)}}\n\n        </div>\n\n        {{/main}}\n        {{/html}}\n        \"\"\"#\n    }\n\n    static func blogHome() -> String {\n        #\"\"\"\n        {{<html}}\n        {{$main}}\n\n        {{& page.contents.html}}\n\n        <div id=\"blog\" class=\"wrapper\">\n\n            {{#empty(context.posts)}}\n            Empty.\n            {{/empty(context.posts)}}\n            {{^empty(context.posts)}}\n            <div id=\"blog-posts\" class=\"grid grid-321\">\n            {{#context.posts}}\n                {{> partials.blog.post}}\n            {{/context.posts}}\n            </div>\n            {{/empty(context.posts)}}\n\n            <br>\n            <a href=\"/articles/page/1\" class=\"cta\">Browse all articles</a>\n\n\n            <h2>Tags</h2>\n            <br>\n            {{#empty(context.tags)}}\n            Empty.\n            {{/empty(context.tags)}}\n            {{^empty(context.tags)}}\n                <div class=\"grid grid-221\">\n            {{#context.tags}}\n                {{> partials.blog.tag}}\n            {{/context.tags}}\n            </div>\n            {{/empty(context.tags)}}\n\n            <h2>Authors</h2>\n            <br>\n            {{#empty(context.authors)}}\n            Empty.\n            {{/empty(context.authors)}}\n            {{^empty(context.authors)}}\n                <div class=\"grid grid-221\">\n            {{#context.authors}}\n                {{> partials.blog.author}}\n            {{/context.authors}}\n            </div>\n            {{/empty(context.authors)}}\n\n        </div>\n        {{/main}}\n        {{/html}}\n        \"\"\"#\n    }\n\n    static func tag() -> String {\n        #\"\"\"\n        {{<html}}\n        {{$main}}\n\n        <div id=\"tag\">\n            <header>\n                {{#page.image}}<img class=\"medium\" src=\"{{.}}\" alt=\"{{page.title}}\">{{/page.image}}\n                <h1>{{page.title}}</h1>\n                <hr>\n                <p>{{page.description}}</p>\n                <p>{{count(page.posts)}} articles</p>\n            </header>\n\n            {{& page.contents.html}}\n\n            {{#empty(page.posts)}}\n            Empty.\n            {{/empty(page.posts)}}\n            {{^empty(page.posts)}}\n            <div class=\"grid grid-321\">\n            {{#page.posts}}\n                {{> partials.blog.post}}\n            {{/page.posts}}\n            </div>\n            {{/empty(page.posts)}}\n        </div>\n\n        {{/main}}\n        {{/html}}\n        \"\"\"#\n    }\n\n    static func author() -> String {\n        #\"\"\"\n        {{<html}}\n        {{$main}}\n        <div id=\"author-page\">\n\n            <header>\n                {{#page.image}}<img class=\"large rounded\" src=\"{{.}}\" alt=\"{{page.title}}\">{{/page.image}}\n                <h1>{{page.title}}</h1>\n                <hr>\n                <p>{{page.description}}</p>\n                <p>{{count(page.posts)}} articles</p>\n            </header>\n\n            {{& page.contents.html}}\n\n            {{#empty(page.posts)}}\n            Empty.\n            {{/empty(page.posts)}}\n            {{^empty(page.posts)}}\n            <div class=\"grid grid-321\">\n            {{#page.posts}}\n                {{> partials.blog.post}}\n            {{/page.posts}}\n            </div>\n            {{/empty(page.posts)}}\n\n        </div>\n\n        {{/main}}\n        {{/html}}\n\n        \"\"\"#\n    }\n\n    static func category() -> String {\n        #\"\"\"\n        {{<html}}\n        {{$main}}\n        <div id=\"docs\">\n            <div class=\"left\">\n                {{> partials.docs.categories }}\n            </div>\n            <div class=\"center\">\n                <article>\n                    <a href=\"/docs/\">Docs</a>\n                    {{& page.contents.html}}\n\n                    {{#empty(page.guides)}}\n                    {{/empty(page.guides)}}\n                    {{^empty(page.guides)}}\n                    <h2>Guides</h2>\n                    <ul>\n                    {{#page.guides}}\n                        <li><a href=\"{{permalink}}\">{{title}}</a></li>\n                    {{/page.guides}}\n                    </ul>\n                    {{/empty(page.guides)}}\n                </article>\n            </div>\n            <div class=\"right\">\n                {{> partials.outline }}\n            </div>\n        </div>\n        {{/main}}\n        {{/html}}\n        \"\"\"#\n    }\n\n    static func guide() -> String {\n        #\"\"\"\n        {{<html}}\n        {{$main}}\n\n        <div id=\"docs\">\n            <div class=\"left\">\n                {{> partials.docs.categories }}\n            </div>\n            <div class=\"center\">\n                <article>\n                    {{#page.category}}\n                    <a href=\"{{permalink}}\">{{title}}</a>\n                    {{/page.category}}\n\n                    {{& page.contents.html}}\n\n\n                    <section>\n                    <div class=\"grid grid-2\">\n                    {{^page.guide.prev}}\n                    <div></div>\n                    {{/page.guide.prev}}\n                    {{#page.guide.prev}}\n                    <div class=\"prev\">\n                        <h4>&larr; Prev guide</h4>\n                        <small>{{category.title}}</small>\n                        <a href=\"{{permalink}}\">{{title}}</a>\n                    </div>\n                    {{/page.guide.prev}}\n\n                    {{^page.guide.next}}\n                    <div></div>\n                    {{/page.guide.next}}\n                    {{#page.guide.next}}\n                    <div class=\"next\">\n                        <h4 style=\"text-align: right;\">Next guide &rarr;</h4>\n                        <small>{{category.title}}</small>\n                        <a href=\"{{permalink}}\">{{title}}</a>\n                    </div>\n                    {{/page.guide.next}}\n                    </div>\n                    </section>\n\n                </article>\n            </div>\n            <div class=\"right\">\n                {{> partials.outline }}\n            </div>\n        </div>\n        {{/main}}\n        {{/html}}\n        \"\"\"#\n    }\n\n    static func docsHome() -> String {\n        #\"\"\"\n        {{<html}}\n        {{$main}}\n        <div id=\"docs\">\n            <div class=\"left\">\n                {{> partials.docs.categories }}\n            </div>\n            <div class=\"center\">\n                <article>\n                    {{& page.contents.html}}\n\n                </article>\n            </div>\n            <div class=\"right\">\n                {{> partials.outline }}\n            </div>\n        </div>\n        {{/main}}\n        {{/html}}\n\n        \"\"\"#\n    }\n\n    static func home() -> String {\n        #\"\"\"\n        {{<html}}\n        {{$main}}\n\n        {{& page.contents.html}}\n\n        <div class=\"centered\">\n        <h2>Most recent</h2>\n\n        <p>Latest static site generator news, Toucan updates and releases.</p>\n        </div>\n        {{#empty(context.posts)}}\n        Empty.\n        {{/empty(context.posts)}}\n        {{^empty(context.posts)}}\n        <div class=\"grid grid-321\">\n        {{#context.posts}}\n            {{> partials.blog.post}}\n        {{/context.posts}}\n        </div>\n        {{/empty(context.posts)}}\n\n        <div class=\"centered\">\n            <br>\n        <a href=\"/articles/page/1/\" class=\"cta\">Browse all articles</a>\n        </div>\n\n        {{/main}}\n        {{/html}}\n        \"\"\"#\n    }\n\n    static func partialAuthor() -> String {\n        #\"\"\"\n        <div class=\"card centered\">\n            {{#image}}\n            <a href=\"{{permalink}}\">\n                <img class=\"large rounded\" src=\"{{.}}\" alt=\"{{title}}\">\n            </a>\n            {{/image}}\n            <h2><a href=\"{{permalink}}\">{{title}}</a></h2>\n            <p>{{count(posts)}} articles</p>\n        </div>\n        \"\"\"#\n    }\n\n    static func partialPost() -> String {\n        #\"\"\"\n        <div class=\"post card\">\n            {{#featured}}<span class=\"featured\">featured</span>{{/featured}}\n\n            {{#image}}\n            <a href=\"{{permalink}}\" target=\"\">\n                <img src=\"{{image}}\" alt=\"{{title}}\">\n            </a>\n            {{/image}}\n            <div class=\"meta\">\n                <time datetime=\"{{publication.formats.iso8601}}\">{{publication.date.short}}</time>\n                {{#contents.readingTime}} &middot; <span class=\"reading-time\">{{.}} min read</span>{{/contents.readingTime}}\n            </div>\n\n            <h2 class=\"title\"><a href=\"{{permalink}}\" target=\"\">{{title}}</a></h2>\n            <hr>\n            <p>{{description}}</p>\n\n            <div class=\"grid grid-221\">\n                <div class=\"author-list\">\n                {{#authors}}\n                    <a href=\"{{permalink}}\">\n                    {{#image}}<img class=\"small rounded\" src=\"{{image}}\" alt=\"{{title}}\">{{/image}}\n                    </a>\n                {{/authors}}\n                </div>\n\n                <div class=\"tag-list\">\n                {{#tags}}\n                    <a href=\"{{permalink}}\"><small>{{title}}</small></a>\n                {{/tags}}\n                </div>\n            </div>\n        </div>\n\n        \"\"\"#\n    }\n\n    static func partialTag() -> String {\n        #\"\"\"\n        <div class=\"card centered\">\n            {{#image}}\n            <a href=\"{{permalink}}\">\n                <img class=\"medium\" src=\"{{.}}\" alt=\"{{title}}\">\n            </a>\n            {{/image}}\n            <h2><a href=\"{{permalink}}\">{{title}}</a></h2>\n            <p>{{count(posts)}} articles</p>\n        </div>\n\n        \"\"\"#\n    }\n\n    static func partialCategories() -> String {\n        #\"\"\"\n        <aside>\n            <ul>\n            {{#context.categories}}\n                <li class=\"category\">\n                    <a href=\"{{permalink}}\">{{title}}</a>\n                    <ul>\n                    {{#guides}}\n                        <li><a href=\"{{permalink}}\">{{title}}</a></li>\n                    {{/guides}}\n                    </ul>\n                </li>\n            {{/context.categories}}\n            </ul>\n        </aside>\n\n        \"\"\"#\n    }\n\n    static func partialCategory() -> String {\n        #\"\"\"\n        <a href=\"{{permalink}}\">\n        <h2 class=\"title\">{{title}}</h2>\n        </a>\n        \"\"\"#\n    }\n\n    static func partialGuide() -> String {\n        #\"\"\"\n        <a href=\"{{permalink}}\">\n        <h2 class=\"title\">{{title}}</h2>\n        </a>\n        \"\"\"#\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSDKTests/Mocks/Mocks.swift",
    "content": "//\n//  Mocks.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 01. 30..\n//\n\nenum Mocks {\n    enum ContentTypes {}\n    enum RawContents {}\n    enum Pipelines {}\n    enum Templates {}\n    enum Views {}\n    enum Blocks {}\n    enum E2E {}\n}\n"
  },
  {
    "path": "Tests/ToucanSDKTests/Template/TemplateValidatorTestSuite.swift",
    "content": "//\n//  TemplateValidatorTestSuite.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 23..\n//\n//\nimport Foundation\nimport Logging\nimport Testing\n@testable import ToucanCore\n@testable import ToucanSDK\n@testable import ToucanSource\n\nimport Version\n\n@Suite\nstruct TemplateValidatorTestSuite {\n\n    @Test\n    func valid() throws {\n        let items:\n            [(\n                String,\n                Template.Metadata.GeneratorVersion.ComparisonType,\n                String\n            )] = [\n                // .exact\n                (\"1.0.0-beta.6\", .exact, \"1.0.0-beta.6\"),\n                (\"1.2.0\", .exact, \"1.2.0\"),\n\n                // .upNextMinor\n                (\"1.0.0-beta.6\", .upNextMinor, \"1.0.0\"),\n                (\"1.0.0-beta.6\", .upNextMinor, \"1.0.0-rc.1\"),\n                (\"1.0.0\", .upNextMinor, \"1.0.1\"),\n\n                // .upNextMajor\n                (\"1.0.0-beta.6\", .upNextMajor, \"1.0.0\"),\n                (\"1.0.0-beta.6\", .upNextMajor, \"1.0.0-rc.1\"),\n                (\"1.0.0\", .upNextMajor, \"1.0.1\"),\n                (\"1.0.0\", .upNextMajor, \"1.2.0\"),\n            ]\n\n        for item in items {\n            let generatorVersion = Version(item.0)!\n            let toucanVersion = Version(item.2)!\n\n            let templateValidator = try TemplateValidator(\n                generatorInfo: .init(version: toucanVersion.description)\n            )\n\n            try templateValidator.validate(\n                Mocks.Templates.example(\n                    generatorVersion: .init(\n                        value: generatorVersion,\n                        type: item.1\n                    )\n                )\n            )\n        }\n    }\n\n    @Test\n    func unsupportedVersion() throws {\n        let items:\n            [(\n                String,\n                Template.Metadata.GeneratorVersion.ComparisonType,\n                String\n            )] = [\n                // .exact\n                (\"1.0.0\", .exact, \"1.0.0-beta.1\"),\n                (\"2.0.1\", .exact, \"2.0.0\"),\n\n                // .upNextMinor\n                (\"1.0.0\", .upNextMinor, \"1.0.1\"),\n                (\"1.0.0\", .upNextMinor, \"1.0.2-beta.1\"),\n                (\"1.0.0\", .upNextMinor, \"1.1.0\"),\n                (\"1.0.0\", .upNextMinor, \"1.0.2-beta.1\"),\n                (\"1.0.0\", .upNextMinor, \"2.0.0\"),\n                (\"1.0.0-beta.6\", .upNextMinor, \"1.0.0-rc.1\"),\n                (\"1.1.1\", .upNextMinor, \"1.0.0\"),\n\n                // .upNextMajor\n                (\"1.0.0\", .upNextMajor, \"1.0.1\"),\n                (\"1.0.0\", .upNextMajor, \"1.2.0\"),\n                (\"1.0.0\", .upNextMajor, \"1.5.0-beta.2\"),\n                (\"1.0.0\", .upNextMajor, \"2.0.0\"),\n                (\"2.0.0\", .upNextMajor, \"1.0.0\"),\n                (\"1.5.0\", .upNextMajor, \"1.0.0-beta.3\"),\n            ]\n\n        for item in items {\n            let generatorVersion = Version(item.0)!\n            let toucanVersion = Version(item.2)!\n\n            let templateValidator = try TemplateValidator(\n                generatorInfo: .init(version: toucanVersion.description)\n            )\n\n            do {\n                try templateValidator.validate(\n                    Mocks.Templates.example(\n                        generatorVersion: .init(\n                            value: generatorVersion,\n                            type: item.1\n                        )\n                    )\n                )\n            }\n            catch {\n                guard\n                    case let .unsupportedGeneratorVersion(\n                        version,\n                        currentVersion\n                    ) = error\n                else {\n                    Issue.record(\n                        \"Expected .unsupportedGeneratorVersion error, got: \\(error)\"\n                    )\n                    return\n                }\n\n                #expect(version.value == generatorVersion)\n                #expect(currentVersion == toucanVersion)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSDKTests/Toucan/ToucanTestSuite.swift",
    "content": "//\n//  ToucanTestSuite.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 06. 20..\n//\n\nimport FileManagerKitBuilder\nimport Foundation\nimport Logging\nimport Testing\nimport ToucanCore\n@testable import ToucanSDK\nimport ToucanSource\n\n@Suite\nstruct ToucanTestSuite {\n\n    @Test\n    func absoluteURLResolution() throws {\n        let fileManager = FileManager.default\n        let homeURL = fileManager.homeDirectoryForCurrentUser\n        let cwd = fileManager.currentDirectoryPath\n        let cwdURL = URL(filePath: cwd)\n        let toucan = Toucan()\n\n        let path1 = toucan.absoluteURL(for: \".\").path()\n        let exp1 = cwdURL.path()\n        #expect(path1 == exp1)\n\n        let path2 = toucan.absoluteURL(for: \"/foo/bar\").path()\n        let exp2 = URL(filePath: \"/foo/bar\").path()\n        #expect(path2 == exp2)\n\n        let path3 = toucan.absoluteURL(for: \"../foo/bar\").path()\n        let exp3 = cwdURL.appending(path: \"../foo/bar\").standardized.path()\n        #expect(path3 == exp3)\n\n        let path4 = toucan.absoluteURL(for: \"../foo/../bar\").path()\n        let exp4 = cwdURL.appending(path: \"../foo/../bar\").standardized.path()\n        #expect(path4 == exp4)\n\n        let path5 = toucan.absoluteURL(for: \"./foo/../bar\").path()\n        let exp5 = cwdURL.appending(path: \"./foo/../bar\").standardized.path()\n        #expect(path5 == exp5)\n\n        let path6 = toucan.absoluteURL(for: \"~/../bar\").path()\n        let exp6 = homeURL.appending(path: \"../bar\").standardized.path()\n        #expect(path6 == exp6)\n\n        let path7 = toucan.absoluteURL(for: \"bar\").path()\n        let exp7 = cwdURL.appending(path: \"bar\").standardized.path()\n        #expect(path7 == exp7)\n\n        let path8 = toucan.absoluteURL(for: \"\").path()\n        let exp8 = cwdURL.appending(path: \"\").path().dropTrailingSlash()\n        #expect(path8 == exp8)\n    }\n\n    @Test\n    func homeURLResolution() throws {\n        let fileManager = FileManager.default\n        let homeURL = fileManager.homeDirectoryForCurrentUser\n        let toucan = Toucan()\n\n        let path1 = toucan.resolveHomeURL(for: \"~/foo\").path()\n        let exp1 = homeURL.appending(path: \"foo\").path()\n        #expect(path1 == exp1)\n\n        let path2 = toucan.resolveHomeURL(for: \"~/\").path()\n        let exp2 = homeURL.appending(path: \"\").path()\n        #expect(path2 == exp2)\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSDKTests/Utilities/AnyCodableWrapTests.swift",
    "content": "//\n//  AnyCodableWrapTests.swift\n//  Toucan\n//\n//  Created by gerp83 on 2025. 04. 28..\n//\n\nimport Foundation\nimport Testing\nimport ToucanSource\nimport ToucanSDK\n\n@Suite\nstruct AnyCodableWrapTests {\n\n    @Test\n    func testWraps() throws {\n        let boolValue = true\n        let intValue = 100\n        let doubleValue = 100.1\n        let stringValue = \"string\"\n        let nilValue: String? = nil\n        let arrayValue = [AnyCodable(\"string\"), AnyCodable(\"string2\")]\n        let dictValue = [\"key\": AnyCodable(\"value\")]\n        let dictValue2 = [\"key\": \"value\", \"key2\": \"value2\"]\n\n        #expect(wrap(boolValue) == AnyCodable(true))\n        #expect(wrap(intValue) == AnyCodable(100))\n        #expect(wrap(doubleValue) == AnyCodable(100.1))\n        #expect(wrap(stringValue) == AnyCodable(\"string\"))\n        #expect(wrap(nilValue) == AnyCodable(nil))\n        #expect(\n            wrap(arrayValue)\n                == AnyCodable([AnyCodable(\"string\"), AnyCodable(\"string2\")])\n        )\n        #expect(wrap(dictValue) == AnyCodable([\"key\": AnyCodable(\"value\")]))\n        #expect(\n            wrap(dictValue2)\n                == AnyCodable([\n                    \"key\": AnyCodable(\"value\"), \"key2\": AnyCodable(\"value2\"),\n                ])\n        )\n    }\n\n    @Test\n    func testUnwraps() throws {\n        let boolValue = AnyCodable(true)\n        let intValue = AnyCodable(100)\n        let doubleValue = AnyCodable(100.1)\n        let stringValue = AnyCodable(\"string\")\n        let nilValue = AnyCodable(nil)\n        let arrayValue = AnyCodable([\n            AnyCodable(\"string\"), AnyCodable(\"string2\"),\n        ])\n        let dictValue = AnyCodable([\n            \"key\": AnyCodable(\"value\"), \"key2\": AnyCodable(\"value2\"),\n        ])\n        let dictValue2 = AnyCodable([\"key\": 100, \"key2\": 200])\n\n        #expect(unwrap(boolValue) as? Bool == true)\n        #expect(unwrap(intValue) as? Int == 100)\n        #expect(unwrap(doubleValue) as? Double == 100.1)\n        #expect(unwrap(stringValue) as? String == \"string\")\n        #expect(unwrap(nilValue) == nil)\n        #expect(unwrap(arrayValue) as? [String] == [\"string\", \"string2\"])\n        #expect(\n            unwrap(dictValue) as? [String: String] == [\n                \"key2\": \"value2\", \"key\": \"value\",\n            ]\n        )\n        #expect(\n            unwrap(dictValue2) as? [String: Int] == [\"key\": 100, \"key2\": 200]\n        )\n    }\n\n}\n"
  },
  {
    "path": "Tests/ToucanSDKTests/Utilities/CopyManagerTestSuite.swift",
    "content": "//\n//  CopyManagerTestSuite.swift\n//  Toucan\n//\n//  Created by Viasz-Kádi Ferenc on 2025. 03. 04..\n//\n\nimport FileManagerKitBuilder\nimport Foundation\nimport Testing\nimport ToucanSDK\n\n@Suite\nstruct CopyManagerTestSuite {\n    @Test()\n    func copyItemsRecursively() async throws {\n        try FileManagerPlayground {\n            Directory(name: \"src\") {\n                Directory(name: \"assets\") {\n                    Directory(name: \"icons\") {\n                        \"foo.svg\"\n                        \"bar.ico\"\n                    }\n                    Directory(name: \"images\") {\n                        \"image.png\"\n                        \"cover.jpg\"\n                    }\n                }\n            }\n            Directory(name: \"workDir\") {}\n        }\n        .test {\n            let src = $1.appendingPathIfPresent(\"src/assets\")\n            let workDirURL = $1.appendingPathIfPresent(\"workDir\")\n\n            let copyManager = CopyManager(\n                fileManager: $0,\n                sources: [\n                    src\n                ],\n                destination: workDirURL\n            )\n            try copyManager.copy()\n\n            #expect(\n                $0.listDirectory(\n                    at: workDirURL.appendingPathIfPresent(\n                        \"icons\"\n                    )\n                )\n                .sorted()\n                    == [\n                        \"foo.svg\",\n                        \"bar.ico\",\n                    ]\n                    .sorted()\n            )\n\n            #expect(\n                $0.listDirectory(\n                    at: workDirURL.appendingPathIfPresent(\n                        \"images\"\n                    )\n                )\n                .sorted()\n                    == [\n                        \"image.png\",\n                        \"cover.jpg\",\n                    ]\n                    .sorted()\n            )\n        }\n    }\n\n    @Test()\n    func copyEmptyDirectory() async throws {\n        try FileManagerPlayground {\n            Directory(name: \"src\") {\n                Directory(name: \"assets\") {}\n            }\n            Directory(name: \"workDir\") {}\n        }\n        .test {\n            let src = $1.appendingPathIfPresent(\"src/assets\")\n            let workDirURL = $1.appendingPathIfPresent(\"workDir\")\n\n            let copyManager = CopyManager(\n                fileManager: $0,\n                sources: [\n                    src\n                ],\n                destination: workDirURL\n            )\n            try copyManager.copy()\n            #expect($0.listDirectory(at: workDirURL).isEmpty)\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSDKTests/Utilities/PrettyPrint.swift",
    "content": "//\n//  PrettyPrint.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 02. 11..\n//\n\nimport Foundation\nimport ToucanSource\n\n/// Pretty prints a `[String: AnyCodable]` dictionary as JSON to standard output.\n/// - Parameter object: A dictionary of key-value pairs with dynamic `AnyCodable` values.\npublic func prettyPrint(\n    _ object: [String: AnyCodable]\n) {\n    let encoder = JSONEncoder()\n    encoder.outputFormatting = [\n        .prettyPrinted,\n        .withoutEscapingSlashes,\n        // .sortedKeys, // Enable if key ordering is desired\n    ]\n\n    do {\n        let data = try encoder.encode(object)\n\n        guard let value = String(data: data, encoding: .utf8) else {\n            return\n        }\n        print(value)\n    }\n    catch {\n        fatalError(error.localizedDescription)\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSDKTests/Utilities/RecursiveMergeTests.swift",
    "content": "//\n//  RecursiveMergeTests.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 04. 15..\n//\n\nimport Testing\nimport ToucanSource\nimport ToucanSDK\n\n@Suite\nstruct RecursiveMergeTests {\n\n    @Test\n    func testBasicMerge() throws {\n        let a: [String: AnyCodable] = [\n            \"foo\": \"a\"\n\n        ]\n        let b: [String: AnyCodable] = [\n            \"foo\": \"b\"\n        ]\n\n        let c = a.recursivelyMerged(with: b)\n\n        #expect(c[\"foo\"] == \"b\")\n    }\n\n    @Test\n    func testComplexMerge() throws {\n        let a: [String: Any] = [\n            \"foo\": \"a\",\n            \"bar\": [\"a\": AnyCodable(\"b\")],\n\n        ]\n        let b: [String: Any] = [\n            \"foo\": \"b\",\n            \"bar\": [\"c\": AnyCodable(\"d\")],\n        ]\n        let c = a.recursivelyMerged(with: b)\n        let expected: [String: Any] = [\n            \"foo\": \"b\",\n            \"bar\": [\n                \"c\": AnyCodable(\"d\"),\n                \"a\": AnyCodable(\"b\"),\n            ],\n        ]\n        #expect(c[\"foo\"] as? String == expected[\"foo\"] as? String)\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSDKTests/Utilities/SlugTests.swift",
    "content": "//\n//  SlugTests.swift\n//  Toucan\n//\n//  Created by gerp83 on 2025. 04. 04..\n//\n\nimport Testing\nimport ToucanSDK\n\n@Suite\nstruct SlugTests {\n    @Test\n    func permalink() throws {\n        let slug = Slug(\"slug\")\n        #expect(\n            slug.permalink(\n                baseURL: \"http://localhost:3000\"\n            ) == \"http://localhost:3000/slug/\"\n        )\n    }\n\n    @Test\n    func permalinkForHomePage() throws {\n        let slug = Slug(\"\")\n        #expect(\n            slug.permalink(\n                baseURL: \"http://localhost:3000\"\n            ) == \"http://localhost:3000/\"\n        )\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSDKTests/Utilities/UnboxingTestSuite.swift",
    "content": "//\n//  UnboxingTestSuite.swift\n//  Toucan\n//\n//  Created by Viasz-Kádi Ferenc on 2025. 05. 09..\n//\n//\n\nimport Foundation\nimport Testing\nimport ToucanSource\nimport ToucanSDK\nimport ToucanMarkdown\n\n@Suite\nstruct UnboxingTests {\n\n    @Test\n    func unboxing() throws {\n        let value: [String: AnyCodable] = [\n            RootContextKeys.context.rawValue: [\n                \"posts\": [\n                    [\n                        \"publication\": AnyCodable(\n                            Optional(\n                                DateContext(\n                                    date: .init(\n                                        full: \"Tuesday, April 15, 2025\",\n                                        long: \"April 15, 2025\",\n                                        medium: \"Apr 15, 2025\",\n                                        short: \"4/15/25\"\n                                    ),\n                                    time: .init(\n                                        full: \"2:00:00 PM Greenwich Mean Time\",\n                                        long: \"2:00:00 PM GMT\",\n                                        medium: \"2:00:00 PM\",\n                                        short: \"2:00 PM\"\n                                    ),\n                                    timestamp: 1744725600.0,\n                                    iso8601: \"2025-04-15T14:00:00.000Z\",\n                                    formats: [\n                                        \"rss\":\n                                            \"Tue, 15 Apr 2025 14:00:00 +0000\",\n                                        \"sitemap\": \"2025-04-15\",\n                                        \"year\": \"2025\",\n                                    ]\n                                )\n                            )\n                        ),\n                        \"description\": AnyCodable(\n                            \"Migration guide for Toucan Beta 3: covering changes to content structure, template changes and rendering features.\"\n                        ),\n                        \"featured\": AnyCodable(true),\n                        PageContextKeys.contents.rawValue: AnyCodable([\n                            PageContentsKeys.readingTime.rawValue: AnyCodable(\n                                2\n                            ),\n                            PageContentsKeys.outline.rawValue: AnyCodable([\n                                AnyCodable(\n                                    Optional(\n                                        Outline(\n                                            level: 2,\n                                            text: \"Changes in contents\",\n                                            fragment: Optional(\n                                                \"changes-in-contents\"\n                                            ),\n                                            children: []\n                                        )\n                                    )\n                                ),\n                                AnyCodable(\n                                    Optional(\n                                        Outline(\n                                            level: 2,\n                                            text: \"Changes in templates\",\n                                            fragment: Optional(\n                                                \"changes-in-templates\"\n                                            ),\n                                            children: []\n                                        )\n                                    )\n                                ),\n                                AnyCodable(\n                                    Optional(\n                                        Outline(\n                                            level: 2,\n                                            text: \"Pipelines\",\n                                            fragment: Optional(\"pipelines\"),\n                                            children: []\n                                        )\n                                    )\n                                ),\n                                AnyCodable(\n                                    Optional(\n                                        Outline(\n                                            level: 2,\n                                            text: \"Useful links\",\n                                            fragment: Optional(\"useful-links\"),\n                                            children: []\n                                        )\n                                    )\n                                ),\n                            ]),\n                            PageContentsKeys.html.rawValue: AnyCodable(\n                                \"<p></p>\"\n                            ),\n                        ]),\n                        \"authors\": AnyCodable([\n                            [\n                                PageContextKeys.contents.rawValue: AnyCodable([\n                                    PageContentsKeys.outline.rawValue:\n                                        AnyCodable([]),\n                                    PageContentsKeys.html.rawValue: AnyCodable(\n                                        \"\"\n                                    ),\n                                    PageContentsKeys.readingTime.rawValue:\n                                        AnyCodable(1),\n                                ]),\n                                PageContextKeys.permalink.rawValue: AnyCodable(\n                                    \"https://toucansites.com/authors/gabor-lengyel/\"\n                                ),\n                                \"description\": AnyCodable(\n                                    \"Former Android Developer, co-founder of Binary Birds Kft.\"\n                                ),\n                                \"slug\": AnyCodable(\n                                    Optional(\n                                        Slug(\n                                            \"authors/gabor-lengyel\"\n                                        )\n                                    )\n                                ),\n                                \"image\": AnyCodable(\n                                    \"https://toucansites.com/assets/authors/gabor-lengyel/gabor-lengyel.jpg\"\n                                ),\n                                \"title\": AnyCodable(\"Gábor Lengyel\"),\n                                \"order\": AnyCodable(10),\n                                SystemPropertyKeys.lastUpdate.rawValue:\n                                    AnyCodable(\n                                        Optional(\n                                            DateContext(\n                                                date: .init(\n                                                    full:\n                                                        \"Friday, April 18, 2025\",\n                                                    long: \"April 18, 2025\",\n                                                    medium: \"Apr 18, 2025\",\n                                                    short: \"4/18/25\"\n                                                ),\n                                                time:\n                                                    .init(\n                                                        full:\n                                                            \"12:45:44 PM Greenwich Mean Time\",\n                                                        long: \"12:45:44 PM GMT\",\n                                                        medium: \"12:45:44 PM\",\n                                                        short: \"12:45 PM\"\n                                                    ),\n                                                timestamp: 1744980344.8431244,\n                                                iso8601:\n                                                    \"2025-04-18T12:45:44.843Z\",\n                                                formats: [\n                                                    \"rss\":\n                                                        \"Fri, 18 Apr 2025 12:45:44 +0000\",\n                                                    \"sitemap\": \"2025-04-18\",\n\n                                                    \"year\": \"2025\",\n                                                ]\n                                            )\n                                        )\n                                    ),\n                            ]\n                        ]),\n                        \"image\": AnyCodable(nil),\n                        \"slug\": AnyCodable(\n                            Optional(Slug(\"beta-3-migration-guide\"))\n                        ),\n                        \"title\": AnyCodable(\"Beta 3 migration guide\"),\n                        PageContextKeys.permalink.rawValue: AnyCodable(\n                            \"https://toucansites.com/beta-3-migration-guide/\"\n                        ),\n                    ]\n                ]\n            ]\n        ]\n\n        let encoder = JSONEncoder()\n        let result = value.unboxed(encoder)\n\n        let firstAuthorSlugValue = result.value(\n            forKeyPath: \"context.posts.0.authors.0.slug\"\n        )\n        let slug = try #require(firstAuthorSlugValue as? Slug)\n        #expect(slug.value == \"authors/gabor-lengyel\")\n\n        let publicationDateFullValue = result.value(\n            forKeyPath: \"context.posts.0.publication.date.full\"\n        )\n        let publicationDateFull = try #require(\n            publicationDateFullValue as? String\n        )\n        #expect(publicationDateFull == \"Tuesday, April 15, 2025\")\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSourceTests/BuildTargetSourceLoaderTestSuite.swift",
    "content": "//\n//  BuildTargetSourceLoaderTestSuite.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 03. 04..\n\nimport FileManagerKit\nimport FileManagerKitBuilder\nimport Foundation\nimport Testing\nimport ToucanCore\nimport ToucanSerialization\n\n@testable import ToucanSource\n\n@Suite\nstruct BuildTargetSourceLoaderTestSuite {\n\n    // MARK: - private helpers\n\n    private func testSourceHierarchy(\n        @FileManagerPlayground.ItemBuilder _ builder: () ->\n            [FileManagerPlayground.Item]\n    ) -> Directory {\n        Directory(name: \"src\", builder)\n    }\n\n    private func testSourceTypesHierarchy(\n        @FileManagerPlayground.ItemBuilder _ builder: () ->\n            [FileManagerPlayground.Item]\n    ) -> Directory {\n        testSourceHierarchy {\n            Directory(name: \"types\", builder)\n        }\n    }\n\n    private func testRawContentLoader(\n        fileManager: FileManagerKit,\n        url: URL\n    ) -> RawContentLoader {\n        let url = url.appending(path: \"src/\")\n        let decoder = ToucanYAMLDecoder()\n        let config = Config.defaults\n        let locations = BuiltTargetSourceLocations(\n            sourceURL: url,\n            config: config\n        )\n        let loader = RawContentLoader(\n            contentsURL: locations.contentsURL,\n            assetsPath: config.contents.assets.path,\n            decoder: .init(),\n            markdownParser: .init(decoder: decoder),\n            fileManager: fileManager\n        )\n        return loader\n    }\n\n    private func testSourceLoader(\n        fileManager: FileManagerKit,\n        url: URL\n    ) -> BuildTargetSourceLoader {\n        let url = url.appending(path: \"src/\")\n        let target = Target.standard\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n        let loader = BuildTargetSourceLoader(\n            sourceURL: url,\n            target: target,\n            fileManager: fileManager,\n            encoder: encoder,\n            decoder: decoder\n        )\n        return loader\n    }\n\n    // MARK: - content types\n\n    @Test()\n    func validContentTypes() async throws {\n        let systemProperties = [\n            SystemPropertyKeys.id.rawValue: Property(\n                propertyType: .string,\n                isRequired: true\n            ),\n            SystemPropertyKeys.lastUpdate.rawValue: Property(\n                propertyType: .string,\n                isRequired: true\n            ),\n            SystemPropertyKeys.slug.rawValue: Property(\n                propertyType: .string,\n                isRequired: true\n            ),\n            SystemPropertyKeys.type.rawValue: Property(\n                propertyType: .string,\n                isRequired: true\n            ),\n        ]\n        let type1 = ContentType(\n            id: \"post\",\n            properties: systemProperties\n        )\n        let type2 = ContentType(\n            id: \"tag\",\n            properties: systemProperties\n        )\n        try FileManagerPlayground {\n            testSourceTypesHierarchy {\n                YAMLFile(name: \"post\", contents: type1)\n                YAMLFile(name: \"tag\", contents: type2)\n            }\n        }\n        .test {\n            let sourceLoader = testSourceLoader(fileManager: $0, url: $1)\n            let config = try sourceLoader.loadConfig()\n            let locations = sourceLoader.getLocations(using: config)\n            let results = try sourceLoader.loadTypes(using: locations)\n\n            let exp: [ContentType] = [type1, type2]\n                .sorted(by: { $0.id < $1.id })\n\n            #expect(results == exp)\n        }\n    }\n\n    @Test()\n    func emptyContentTypes() async throws {\n        try FileManagerPlayground {\n            testSourceTypesHierarchy {}\n        }\n        .test {\n            let sourceLoader = testSourceLoader(fileManager: $0, url: $1)\n            let config = try sourceLoader.loadConfig()\n            let locations = sourceLoader.getLocations(using: config)\n            let results = try sourceLoader.loadTypes(using: locations)\n            #expect(results.isEmpty)\n        }\n    }\n\n    @Test()\n    func invalidContentTypes() async throws {\n        try FileManagerPlayground {\n            testSourceTypesHierarchy {\n                File(\n                    name: \"invalid.yaml\",\n                    string: \"\"\n                )\n            }\n        }\n        .test {\n            do {\n                let sourceLoader = testSourceLoader(fileManager: $0, url: $1)\n                let config = try sourceLoader.loadConfig()\n                let locations = sourceLoader.getLocations(using: config)\n                _ = try sourceLoader.loadTypes(using: locations)\n            }\n            catch let error as SourceLoaderError {\n                #expect(\n                    error.logMessage == \"Could not load: `ContentType`.\"\n                )\n            }\n            catch {\n                Issue.record(\"Invalid error type: `\\(type(of: error))`.\")\n            }\n        }\n    }\n\n    // MARK: - blocks\n\n    @Test\n    func blocks() throws {\n        try FileManagerPlayground {\n            testSourceHierarchy {\n                Directory(name: \"blocks\") {\n                    YAMLFile(\n                        name: \"link\",\n                        contents: Block(\n                            name: \"link\"\n                        )\n                    )\n                    File(\n                        name: \"button.yml\",\n                        string: \"\"\"\n                            name: Button\n                            tag: a\n                            parameters:\n                              - label: url\n                                default: \"\"\n                              - label: class\n                                default: \"button\"\n                              - label: target\n                                default: \"_blank\"\n                            removesChildParagraph: true\n                            attributes:\n                              - name: href\n                                value: \"{{url}}\"\n                              - name: target\n                                value: \"{{target}}\"\n                              - name: class\n                                value: \"{{class}}\"\n                            \"\"\"\n                    )\n                }\n            }\n        }\n        .test {\n            let sourceLoader = testSourceLoader(fileManager: $0, url: $1)\n            let config = try sourceLoader.loadConfig()\n            let locations = sourceLoader.getLocations(using: config)\n            let blocks = try sourceLoader.loadBlocks(using: locations)\n\n            #expect(blocks.count == 2)\n        }\n    }\n\n    // MARK: - valid source files\n\n    @Test()\n    func validSource() async throws {\n        let type1 = ContentType(\n            id: \"post\"\n        )\n        let type2 = ContentType(\n            id: \"tag\"\n        )\n\n        try FileManagerPlayground {\n            testSourceHierarchy {\n                Directory(name: \"contents\") {\n                    \"index.md\"\n                    Directory(name: \"assets\") {\n                        \"main.js\"\n                    }\n                    Directory(name: \"404\") {\n                        \"index.md\"\n                    }\n\n                    Directory(name: \"blog\") {\n                        \"noindex.yml\"\n                        Directory(name: \"authors\") {\n                            \"index.md\"\n                        }\n                    }\n                    Directory(name: \"redirects\") {\n                        \"noindex.yml\"\n                        Directory(name: \"home-old\") {\n                            \"index.md\"\n                        }\n                    }\n                }\n                Directory(name: \"types\") {\n                    YAMLFile(name: \"post\", contents: type1)\n                    YAMLFile(name: \"tag\", contents: type2)\n                }\n                Directory(name: \"blocks\") {\n                    YAMLFile(\n                        name: \"link\",\n                        contents: Block(\n                            name: \"link\"\n                        )\n                    )\n                }\n            }\n        }\n        .test {\n            let sourceLoader = testSourceLoader(fileManager: $0, url: $1)\n            let buildTargetSource = try sourceLoader.load()\n            #expect(buildTargetSource.blocks.count == 1)\n            #expect(buildTargetSource.types.count == 2)\n            #expect(buildTargetSource.rawContents.count == 4)\n        }\n    }\n\n    // MARK: - config with target name\n\n    @Test\n    func configWithTargetName() async throws {\n        var config = Config.defaults\n        config.templates.current.path = \"test\"\n\n        try FileManagerPlayground {\n            testSourceHierarchy {\n                YAMLFile(name: \"config-dev\", contents: config)\n                YAMLFile(name: \"config\", contents: Config.defaults)\n            }\n        }\n        .test {\n            let sourceLoader = testSourceLoader(fileManager: $0, url: $1)\n            let result = try sourceLoader.loadConfig()\n\n            #expect(result.templates.current.path == \"test\")\n        }\n    }\n\n    @Test\n    func invalidConfigWithTargetName() async throws {\n        try FileManagerPlayground {\n            testSourceHierarchy {\n                YAMLFile(name: \"config-dev\", contents: \"invalid\")\n                YAMLFile(name: \"config\", contents: Config.defaults)\n            }\n        }\n        .test {\n            let sourceLoader = testSourceLoader(fileManager: $0, url: $1)\n\n            do {\n                _ = try sourceLoader.loadConfig()\n                Issue.record(\"Invalid target config should throw an error.\")\n            }\n            catch let error as SourceLoaderError {\n                #expect(error.logMessage == \"Could not load: `Config`.\")\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSourceTests/Extensions/FileManagerKitExtensionsTestSuite.swift",
    "content": "//\n//  FileManagerKitExtensionsTestSuite.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 03. 04..\n\nimport FileManagerKitBuilder\nimport Foundation\nimport Testing\n\n@testable import ToucanSource\n\n@Suite\nstruct FileManagerKitExtensionsTestSuite {\n    @Test()\n    func findEmpty() throws {\n        try FileManagerPlayground()\n            .test {\n                let locations = $0.find(at: $1)\n                #expect(locations.isEmpty)\n            }\n    }\n\n    @Test()\n    func findAllFiles() throws {\n        try FileManagerPlayground {\n            Directory(name: \"foo\") {\n                Directory(name: \"bar\") {\n                    \"baz.yaml\"\n                    \"qux.yml\"\n                }\n            }\n        }\n        .test {\n            let url = $1.appending(path: \"foo/bar/\")\n            let locations = $0.find(at: url).sorted()\n\n            #expect(locations == [\"baz.yaml\", \"qux.yml\"])\n        }\n    }\n\n    @Test()\n    func findDirectoriesAndFiles() throws {\n        try FileManagerPlayground {\n            Directory(name: \"foo\") {\n                Directory(name: \"bar\")\n                \"baz.yaml\"\n                \"qux.yml\"\n            }\n        }\n        .test {\n            let url = $1.appending(path: \"foo/\")\n            let locations = $0.find(at: url).sorted()\n\n            #expect(locations == [\"bar\", \"baz.yaml\", \"qux.yml\"])\n        }\n    }\n\n    @Test()\n    func findMultipleExtensions() async throws {\n        try FileManagerPlayground {\n            Directory(name: \"foo\") {\n                Directory(name: \"bar\") {\n                    \"baz.yaml\"\n                    \"qux.yml\"\n                    \"quux.txt\"\n                }\n            }\n        }\n        .test {\n            let url = $1.appending(path: \"foo/bar/\")\n            let locations =\n                $0.find(\n                    extensions: [\n                        \"yml\",\n                        \"yaml\",\n                    ],\n                    at: url\n                )\n                .sorted()\n\n            #expect(locations == [\"baz.yaml\", \"qux.yml\"])\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSourceTests/Files/YAMLFile.swift",
    "content": "//\n//  YAMLFile.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 20..\n//\n\nimport FileManagerKit\nimport FileManagerKitBuilder\nimport ToucanSerialization\n\nstruct YAMLFile<T: Encodable> {\n\n    var name: String\n    var ext: String\n    var contents: T\n\n    init(\n        name: String,\n        ext: String = \"yml\",\n        contents: T\n    ) {\n        self.name = name\n        self.ext = ext\n        self.contents = contents\n    }\n}\n\nextension YAMLFile: BuildableItem {\n\n    func buildItem() -> FileManagerPlayground.Item {\n        let encoder = ToucanYAMLEncoder()\n        return .file(\n            .init(\n                name: name + \".\" + ext,\n                string: try! encoder.encode(contents)\n            )\n        )\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSourceTests/MarkdownParserTestSuite.swift",
    "content": "//\n//  MarkdownParserTestSuite.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 04. 15..\n//\nimport Logging\nimport Testing\nimport ToucanCore\nimport ToucanSerialization\n@testable import ToucanSource\n\n@Suite\nstruct MarkdownParserTestSuite {\n    @Test\n    func basicParserLogic() throws {\n        let logger: Logger = .init(label: \"MarkdownParserTestSuite\")\n        let input = #\"\"\"\n            ---\n            slug: lorem-ipsum\n            title: Lorem ipsum\n            ---\n\n            Lorem ipsum dolor sit amet.\n            \"\"\"#\n\n        let parser = MarkdownParser(\n            decoder: ToucanYAMLDecoder(),\n            logger: logger\n        )\n        let markdown = try parser.parse(input)\n\n        #expect(markdown.frontMatter[\"slug\"] == .init(\"lorem-ipsum\"))\n        #expect(markdown.frontMatter[\"title\"] == .init(\"Lorem ipsum\"))\n    }\n\n    @Test\n    func frontMatterNoContent() throws {\n        let logger: Logger = .init(label: \"MarkdownParserTestSuite\")\n        let input = #\"\"\"\n            ---\n            slug: lorem-ipsum\n            title: Lorem ipsum\n            ---\n            \"\"\"#\n\n        let parser = MarkdownParser(\n            decoder: ToucanYAMLDecoder(),\n            logger: logger\n        )\n        let markdown = try parser.parse(input)\n\n        #expect(markdown.frontMatter[\"slug\"] == .init(\"lorem-ipsum\"))\n        #expect(markdown.frontMatter[\"title\"] == .init(\"Lorem ipsum\"))\n    }\n\n    @Test\n    func frontMatterWithSeparatorInContent() throws {\n        let logger: Logger = .init(label: \"MarkdownParserTestSuite\")\n        let input = #\"\"\"\n            ---\n            slug: lorem-ipsum\n            title: Lorem ipsum\n            ---\n\n            Text with '---' separator as content\n            \"\"\"#\n\n        let parser = MarkdownParser(\n            decoder: ToucanYAMLDecoder(),\n            logger: logger\n        )\n        let markdown = try parser.parse(input)\n\n        #expect(markdown.frontMatter[\"slug\"] == .init(\"lorem-ipsum\"))\n        #expect(markdown.frontMatter[\"title\"] == .init(\"Lorem ipsum\"))\n    }\n\n    @Test\n    func firstMissingSeparator() throws {\n        let logger: Logger = .init(label: \"MarkdownParserTestSuite\")\n        let input = #\"\"\"\n            slug: lorem-ipsum\n            title: Lorem ipsum\n            ---\n\n            Lorem ipsum dolor sit amet.\n            \"\"\"#\n\n        let parser = MarkdownParser(\n            decoder: ToucanYAMLDecoder(),\n            logger: logger\n        )\n        let markdown = try parser.parse(input)\n\n        #expect(markdown.frontMatter.isEmpty)\n    }\n\n    @Test\n    func firstMissingSeparatorWithSeparatorInContent() throws {\n        let logger: Logger = .init(label: \"MarkdownParserTestSuite\")\n        let input = #\"\"\"\n            slug: lorem-ipsum\n            title: Lorem ipsum\n            ---\n\n            Text with '---' separator as content\n            \"\"\"#\n\n        let parser = MarkdownParser(\n            decoder: ToucanYAMLDecoder(),\n            logger: logger\n        )\n        let markdown = try parser.parse(input)\n\n        #expect(markdown.frontMatter.isEmpty)\n    }\n\n    @Test\n    func secondMissingSeparator() throws {\n        let logger: Logger = .init(label: \"MarkdownParserTestSuite\")\n        let input = #\"\"\"\n            ---\n            slug: lorem-ipsum\n            title: Lorem ipsum\n\n            Lorem ipsum dolor sit amet.\n            \"\"\"#\n\n        let parser = MarkdownParser(\n            decoder: ToucanYAMLDecoder(),\n            logger: logger\n        )\n\n        do {\n            _ = try parser.parse(input)\n        }\n        catch let error as ToucanError {\n            if let decodingError = error.lookup(DecodingError.self) {\n                switch decodingError {\n                case let .dataCorrupted(context):\n                    let expected = \"The given data was not valid YAML.\"\n                    #expect(context.debugDescription == expected)\n                default:\n                    throw error\n                }\n            }\n            else {\n                throw error\n            }\n        }\n    }\n\n    @Test\n    func secondMissingSeparatorWithSeparatorInContent() throws {\n        let logger: Logger = .init(label: \"MarkdownParserTestSuite\")\n        let input = #\"\"\"\n            ---\n            slug: lorem-ipsum\n            title: Lorem ipsum\n\n            Text with '---' separator as content\n            \"\"\"#\n\n        let parser = MarkdownParser(\n            decoder: ToucanYAMLDecoder(),\n            logger: logger\n        )\n\n        do {\n            _ = try parser.parse(input)\n        }\n        catch let error as ToucanError {\n            if let context = error.lookup({\n                if case let DecodingError.dataCorrupted(ctx) = $0 {\n                    return ctx\n                }\n                return nil\n            }) {\n                let expected = \"The given data was not valid YAML.\"\n                #expect(context.debugDescription == expected)\n            }\n            else {\n                throw error\n            }\n        }\n    }\n\n    @Test\n    func withManySeparators() throws {\n        let logger: Logger = .init(label: \"MarkdownParserTestSuite\")\n        let input = #\"\"\"\n            --- --- ---\n            slug: lorem-ipsum\n            title: Lorem ipsum\n            --- --- ---\n\n            Text with '---' separator as content\n            \"\"\"#\n\n        let parser = MarkdownParser(\n            decoder: ToucanYAMLDecoder(),\n            logger: logger\n        )\n\n        do {\n            _ = try parser.parse(input)\n        }\n        catch let error as ToucanError {\n            if let context = error.lookup({\n                if case let DecodingError.typeMismatch(_, ctx) = $0 {\n                    return ctx\n                }\n                return nil\n            }) {\n                let exp = \"Expected to decode Mapping but found Node instead.\"\n                #expect(context.debugDescription == exp)\n            }\n            else {\n                throw error\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSourceTests/Models/BuildTargetSourceLocationsTestSuite.swift",
    "content": "//\n//  BuildTargetSourceLocationsTestSuite.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 21..\n//\n\nimport FileManagerKit\nimport FileManagerKitBuilder\nimport Foundation\nimport Testing\nimport ToucanCore\nimport ToucanSerialization\n\n@testable import ToucanSource\n\n@Suite\nstruct BuildTargetSourceLocationsTestSuite {\n\n    @Test()\n    func defaults() async throws {\n        let prefix = \"/src\"\n        let def = \"default\"\n\n        let templatesPath = \"\\(prefix)/templates\"\n        let templatePath = \"\\(templatesPath)/\\(def)\"\n        let overridesPath = \"\\(templatesPath)/overrides/\\(def)\"\n\n        let expectedBase = \"\\(prefix)\"\n        let expectedAssets = \"\\(prefix)/assets\"\n        let expectedSettings = \"\\(prefix)\"\n        let expectedContents = \"\\(prefix)/contents\"\n        let expectedTypes = \"\\(prefix)/types\"\n        let expectedBlocks = \"\\(prefix)/blocks\"\n        let expectedPipelines = \"\\(prefix)/pipelines\"\n        let expectedTemplates = templatesPath\n        let expectedCurrentTemplate = templatePath\n        let expectedTemplateAssets = \"\\(templatePath)/assets\"\n        let expectedTemplateTemplates = \"\\(templatePath)/views\"\n        let expectedOverrides = overridesPath\n        let expectedOverrideAssets = \"\\(overridesPath)/assets\"\n        let expectedOverrideTemplates = \"\\(overridesPath)/views\"\n\n        let url = URL(filePath: prefix)\n        let locations = BuiltTargetSourceLocations(\n            sourceURL: url,\n            config: .defaults\n        )\n\n        let basePath = locations.baseURL.path()\n        let assetsPath = locations.siteAssetsURL.path()\n        let settingsPath = locations.siteSettingsURL.path()\n        let contentsPath = locations.contentsURL.path()\n        let typesPath = locations.typesURL.path()\n        let blocksPath = locations.blocksURL.path()\n        let pipelinesPath = locations.pipelinesURL.path()\n        let templatesPathValue = locations.templatesURL.path()\n        let currentTemplatePath = locations.currentTemplateURL.path()\n        let templateAssetsPath = locations.currentTemplateAssetsURL.path()\n        let templateTemplatesPath = locations.currentTemplateViewsURL.path()\n        let overridesPathValue = locations.currentTemplateOverridesURL.path()\n        let overrideAssetsPath = locations.currentTemplateAssetOverridesURL\n            .path()\n        let overrideTemplatesPath = locations.currentTemplateViewsOverridesURL\n            .path()\n\n        #expect(basePath == expectedBase)\n        #expect(assetsPath == expectedAssets)\n        #expect(settingsPath == expectedSettings)\n        #expect(contentsPath == expectedContents)\n        #expect(typesPath == expectedTypes)\n        #expect(blocksPath == expectedBlocks)\n        #expect(pipelinesPath == expectedPipelines)\n        #expect(templatesPathValue == expectedTemplates)\n        #expect(currentTemplatePath == expectedCurrentTemplate)\n        #expect(templateAssetsPath == expectedTemplateAssets)\n        #expect(templateTemplatesPath == expectedTemplateTemplates)\n        #expect(overridesPathValue == expectedOverrides)\n        #expect(overrideAssetsPath == expectedOverrideAssets)\n        #expect(overrideTemplatesPath == expectedOverrideTemplates)\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSourceTests/Objects/AnyCodableTestSuite.swift",
    "content": "//\n//  AnyCodableTestSuite.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 03. 30..\n\nimport Foundation\nimport Testing\nimport ToucanSerialization\n\n@testable import ToucanSource\n\n@Suite\nstruct AnyCodableTestSuite {\n\n    struct SomeCodable: Codable {\n        enum CodingKeys: String, CodingKey {\n            case string\n            case int\n            case bool\n            case hasUnderscore = \"has_underscore\"\n        }\n\n        var string: String\n        var int: Int\n        var bool: Bool\n        var hasUnderscore: String\n    }\n\n    @Test\n    func decodingInt() throws {\n        let object = \"123\"\n\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(AnyCodable.self, from: object)\n\n        #expect(result.value as? Int == 123)\n    }\n\n    @Test\n    func decodingDouble() throws {\n        let object = \"123.45\"\n\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(AnyCodable.self, from: object)\n\n        #expect(result.value as? Double == 123.45)\n    }\n\n    @Test\n    func decodingBool() throws {\n        let object = \"true\"\n\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(AnyCodable.self, from: object)\n\n        #expect(result.value as? Bool == true)\n    }\n\n    @Test\n    func decodingString() throws {\n        let object = \"Hello\"\n\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(AnyCodable.self, from: object)\n\n        #expect(result.value as? String == \"Hello\")\n    }\n\n    @Test\n    func decodingArray() throws {\n        let object = \"[1, 2, 3]\"\n\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(AnyCodable.self, from: object)\n\n        #expect(result.value as? [Int] == [1, 2, 3])\n    }\n\n    @Test\n    func decodingDictionary() throws {\n        let object = \"\"\"\n            key1: 1 \n            key2: value\n            \"\"\"\n\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(AnyCodable.self, from: object)\n\n        guard let dict = result.value as? [String: AnyCodable] else {\n            Issue.record(\"Result is not a dictionary.\")\n            return\n        }\n        #expect(dict[\"key1\"] == 1)\n        #expect(dict[\"key2\"] == \"value\")\n    }\n\n    @Test\n    func decodingNestedStructures() throws {\n        let object = \"\"\"\n            name: \"Toucan\"\n            description: \"Static Site Generator\"\n            navigation:\n                - label: \"Home\"\n                  url: \"/\"\n            \"\"\"\n\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(AnyCodable.self, from: object)\n\n        guard let dict = result.value as? [String: AnyCodable] else {\n            Issue.record(\"Result is not a dictionary.\")\n            return\n        }\n        #expect(dict[\"name\"] == \"Toucan\")\n    }\n\n    @Test\n    func jSONDecoding() throws {\n        let json = \"\"\"\n            {\n                \"boolean\": true,\n                \"integer\": 42,\n                \"double\": 3.141592653589793,\n                \"string\": \"string\",\n                \"array\": [1, 2, 3],\n                \"dict\": {\n                    \"a\": \"alpha\",\n                    \"b\": \"bravo\",\n                    \"c\": \"charlie\"\n                },\n                \"null\": null\n            }\n            \"\"\"\n            .data(using: .utf8)!\n\n        let decoder = JSONDecoder()\n        let dictionary = try decoder.decode(\n            [String: AnyCodable].self,\n            from: json\n        )\n\n        #expect(dictionary[\"boolean\"]?.value as! Bool == true)\n        #expect(dictionary[\"integer\"]?.value as! Int == 42)\n        #expect(dictionary[\"double\"]?.value as! Double == 3.141592653589793)\n        #expect(dictionary[\"string\"]?.value as! String == \"string\")\n        #expect(dictionary[\"array\"]?.value as! [Int] == [1, 2, 3])\n        #expect(\n            dictionary[\"dict\"]?.value as! [String: AnyCodable] == [\n                \"a\": .init(\"alpha\"),\n                \"b\": .init(\"bravo\"),\n                \"c\": .init(\"charlie\"),\n            ]\n        )\n        #expect(dictionary[\"null\"]?.value == nil)\n    }\n\n    @Test\n    func jSONDecodingEquatable() throws {\n        let json = \"\"\"\n            {\n                \"boolean\": true,\n                \"integer\": 42,\n                \"double\": 3.141592653589793,\n                \"string\": \"string\",\n                \"array\": [1, 2, 3],\n                \"dict\": {\n                    \"a\": \"alpha\",\n                    \"b\": \"bravo\",\n                    \"c\": \"charlie\"\n                },\n                \"null\": null\n            }\n            \"\"\"\n            .data(using: .utf8)!\n\n        let decoder = JSONDecoder()\n        let dictionary1 = try decoder.decode(\n            [String: AnyCodable].self,\n            from: json\n        )\n        let dictionary2 = try decoder.decode(\n            [String: AnyCodable].self,\n            from: json\n        )\n\n        #expect(dictionary1[\"boolean\"] == dictionary2[\"boolean\"])\n        #expect(dictionary1[\"integer\"] == dictionary2[\"integer\"])\n        #expect(dictionary1[\"double\"] == dictionary2[\"double\"])\n        #expect(dictionary1[\"string\"] == dictionary2[\"string\"])\n        #expect(\n            dictionary1[\"array\"]?.value as? [Int] == dictionary2[\"array\"]?.value\n                as? [Int]\n        )\n        #expect(\n            dictionary1[\"dict\"]?.value as? [String: String] == dictionary2[\n                \"dict\"\n            ]?\n            .value as? [String: String]\n        )\n        #expect(dictionary1[\"null\"]?.value == nil)\n        #expect(dictionary2[\"null\"]?.value == nil)\n    }\n\n    @Test\n    func jSONEncoding() throws {\n        let someCodable = AnyCodable(\n            SomeCodable(\n                string: \"String\",\n                int: 100,\n                bool: true,\n                hasUnderscore: \"another string\"\n            )\n        )\n\n        let injectedValue = 1234\n        let dictionary: [String: AnyCodable] = [\n            \"boolean\": true,\n            \"integer\": 42,\n            \"double\": 3.141592653589793,\n            \"string\": \"string\",\n            \"stringInterpolation\": \"string \\(injectedValue)\",\n            \"array\": [1, 2, 3],\n            \"dict\": [\n                \"a\": \"alpha\",\n                \"b\": \"bravo\",\n                \"c\": \"charlie\",\n            ],\n            \"someCodable\": someCodable,\n            \"null\": nil,\n        ]\n\n        let encoder = JSONEncoder()\n        let json = try encoder.encode(dictionary)\n        let encodedJSONObject =\n            try JSONSerialization.jsonObject(with: json, options: [])\n            as! NSDictionary\n\n        let expected = \"\"\"\n            {\n                \"boolean\": true,\n                \"integer\": 42,\n                \"double\": 3.141592653589793,\n                \"string\": \"string\",\n                \"stringInterpolation\": \"string 1234\",\n                \"array\": [1, 2, 3],\n                \"dict\": {\n                    \"a\": \"alpha\",\n                    \"b\": \"bravo\",\n                    \"c\": \"charlie\"\n                },\n                \"someCodable\": {\n                    \"string\": \"String\",\n                    \"int\": 100,\n                    \"bool\": true,\n                    \"has_underscore\": \"another string\"\n                },\n                \"null\": null\n            }\n            \"\"\"\n            .data(using: .utf8)!\n\n        let expectedJSONObject =\n            try JSONSerialization.jsonObject(\n                with: expected,\n                options: []\n            ) as! NSDictionary\n\n        #expect(encodedJSONObject == expectedJSONObject)\n    }\n\n    @Test\n    func allValues() throws {\n        let boolValue = AnyCodable(true)\n        let intValue = AnyCodable(100)\n        let doubleValue = AnyCodable(100.1)\n        let stringValue = AnyCodable(\"string\")\n        let nilValue = AnyCodable(nil)\n        let arrayValue = AnyCodable([\n            AnyCodable(\"string\"), AnyCodable(\"string2\"),\n        ])\n        let dictValue = AnyCodable([\"key\": AnyCodable(\"value\")])\n\n        // test values\n        #expect(boolValue.boolValue() == true)\n        #expect(intValue.intValue() == 100)\n        #expect(doubleValue.doubleValue() == 100.1)\n        #expect(stringValue.stringValue() == \"string\")\n        #expect(nilValue.stringValue() == nil)\n        #expect(\n            arrayValue.arrayValue(as: AnyCodable.self) == [\n                AnyCodable(\"string\"), AnyCodable(\"string2\"),\n            ]\n        )\n        #expect(dictValue.dictValue() == [\"key\": AnyCodable(\"value\")])\n        #expect(arrayValue.arrayValue(as: Int.self) == [])\n        #expect(arrayValue.dictValue() == [:])\n\n        // test description/debugDescription\n        #expect(boolValue.description == \"true\")\n        #expect(boolValue.debugDescription == \"AnyCodable(true)\")\n        #expect(intValue.description == \"100\")\n        #expect(intValue.debugDescription == \"AnyCodable(100)\")\n        #expect(doubleValue.description == \"100.1\")\n        #expect(doubleValue.debugDescription == \"AnyCodable(100.1)\")\n        #expect(stringValue.description == \"string\")\n        #expect(stringValue.debugDescription == \"AnyCodable(\\\"string\\\")\")\n        #expect(nilValue.description == \"nil\")\n        #expect(nilValue.debugDescription == \"AnyCodable(nil)\")\n        #expect(\n            arrayValue.description\n                == \"[AnyCodable(\\\"string\\\"), AnyCodable(\\\"string2\\\")]\"\n        )\n        #expect(\n            arrayValue.debugDescription\n                == \"AnyCodable([AnyCodable(\\\"string\\\"), AnyCodable(\\\"string2\\\")])\"\n        )\n        #expect(dictValue.description == \"[\\\"key\\\": AnyCodable(\\\"value\\\")]\")\n        #expect(\n            dictValue.debugDescription\n                == \"AnyCodable([\\\"key\\\": AnyCodable(\\\"value\\\")])\"\n        )\n\n        // hash\n        _ = boolValue.hashValue\n        _ = intValue.hashValue\n        _ = doubleValue.hashValue\n        _ = stringValue.hashValue\n        _ = nilValue.hashValue\n        _ = arrayValue.hashValue\n        _ = dictValue.hashValue\n    }\n\n    @Test\n    func allComparisons() throws {\n        let boolValue = AnyCodable(true)\n        let boolValue2 = AnyCodable(true)\n        let intValue = AnyCodable(100)\n        let intValue2 = AnyCodable(100)\n        let doubleValue = AnyCodable(100.1)\n        let doubleValue2 = AnyCodable(100.1)\n        let stringValue = AnyCodable(\"string\")\n        let stringValue2 = AnyCodable(\"string\")\n        let nilValue = AnyCodable(nil)\n        let nilValue2 = AnyCodable(nil)\n        let arrayValue = AnyCodable([\n            AnyCodable(\"string\"), AnyCodable(\"string2\"),\n        ])\n        let arrayValue2 = AnyCodable([\n            AnyCodable(\"string\"), AnyCodable(\"string2\"),\n        ])\n        let dictValue = AnyCodable([\"key\": AnyCodable(\"value\")])\n        let dictValue2 = AnyCodable([\"key\": AnyCodable(\"value\")])\n\n        #expect(boolValue == boolValue2)\n        #expect(intValue == intValue2)\n        #expect(doubleValue == doubleValue2)\n        #expect(stringValue == stringValue2)\n        #expect(nilValue == nilValue2)\n        #expect(arrayValue == arrayValue2)\n        #expect(dictValue == dictValue2)\n        #expect(dictValue != arrayValue)\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSourceTests/Objects/Config/ConfigTestSuite.swift",
    "content": "//\n//  ConfigTestSuite.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 04. 17..\n\nimport Foundation\nimport Testing\nimport ToucanSerialization\n\n@testable import ToucanSource\n\n@Suite\nstruct ConfigTestSuite {\n    @Test\n    func defaults() throws {\n        let object = Config.defaults\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n\n        let value1: String = try encoder.encode(object)\n        let result1 = try decoder.decode(Config.self, from: value1)\n\n        let value2: Data = try encoder.encode(object)\n        let result2 = try decoder.decode(Config.self, from: value2)\n\n        #expect(object == result1)\n        #expect(object == result2)\n    }\n\n    @Test\n    func empty() throws {\n        let value = \"\"\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(Config.self, from: value)\n        let expectation = Config.defaults\n\n        #expect(result == expectation)\n    }\n\n    @Test\n    func custom() throws {\n        let value = \"\"\"\n            blocks:\n              path: custom1\n            contents:\n              assets:\n                path: custom2\n              path: custom3\n            dataTypes:\n              date:\n                formats:\n                  test1:\n                    format: his\n                    locale: hu-HU\n                    timeZone: CET\n                input:\n                  format: ymd\n                output:\n                  locale: en-GB\n                  timeZone: PST\n            pipelines:\n              path: custom4\n            renderer:\n              outlineLevels:\n              - 4\n              paragraphStyles:\n                test:\n                - test1\n              wordsPerMinute: 42\n            site:\n              assets:\n                path: custom5\n              settings:\n                path: custom6\n            templates:\n              assets:\n                path: custom7\n              current:\n                path: custom8\n              location:\n                path: custom9\n              overrides:\n                path: custom10\n              views:\n                path: custom11\n            types:\n              path: custom12\n            \"\"\" + \"\\n\"\n\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(Config.self, from: value)\n\n        var expectation = Config.defaults\n        expectation.blocks.path = \"custom1\"\n        expectation.contents.assets.path = \"custom2\"\n        expectation.contents.path = \"custom3\"\n        expectation.dataTypes.date.input = .init(\n            localization: .defaults,\n            format: \"ymd\"\n        )\n        expectation.dataTypes.date.output = .init(\n            locale: \"en-GB\",\n            timeZone: \"PST\"\n        )\n        expectation.dataTypes.date.formats[\"test1\"] = .init(\n            localization: .init(\n                locale: \"hu-HU\",\n                timeZone: \"CET\"\n            ),\n            format: \"his\"\n        )\n        expectation.pipelines.path = \"custom4\"\n        expectation.renderer.outlineLevels = [4]\n        expectation.renderer.paragraphStyles.styles = [\n            \"test\": [\n                \"test1\"\n            ]\n        ]\n        expectation.renderer.wordsPerMinute = 42\n        expectation.site.assets.path = \"custom5\"\n        expectation.site.settings.path = \"custom6\"\n        expectation.templates.assets.path = \"custom7\"\n        expectation.templates.current.path = \"custom8\"\n        expectation.templates.location.path = \"custom9\"\n        expectation.templates.overrides.path = \"custom10\"\n        expectation.templates.views.path = \"custom11\"\n        expectation.types.path = \"custom12\"\n\n        #expect(result.blocks == expectation.blocks)\n        #expect(result.contents == expectation.contents)\n        #expect(result.dataTypes == expectation.dataTypes)\n        #expect(result.pipelines == expectation.pipelines)\n        #expect(result.renderer == expectation.renderer)\n        #expect(result.site == expectation.site)\n        #expect(result.templates == expectation.templates)\n        #expect(result.types == expectation.types)\n\n        let encodedValue: String = try encoder.encode(expectation)\n\n        #expect(result == expectation)\n        #expect(value == encodedValue)\n    }\n\n    @Test\n    func invalidKey() throws {\n        let value = \"\"\"\n            dataTypesss:\n              date:\n                input:\n                  format: ymd\n            \"\"\" + \"\\n\"\n\n        let decoder = ToucanYAMLDecoder()\n\n        do {\n            let _ = try decoder.decode(Config.self, from: value)\n        }\n        catch {\n            if let context = error.lookup({\n                if case let DecodingError.dataCorrupted(ctx) = $0 {\n                    return ctx\n                }\n                return nil\n            }) {\n                let expected =\n                    \"Unknown keys found: `dataTypesss`. Expected keys: `blocks`, `contents`, `dataTypes`, `pipelines`, `renderer`, `site`, `templates`, `types`.\"\n                #expect(context.debugDescription == expected)\n            }\n            else {\n                throw error\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSourceTests/Objects/DateFormatting/DateFormattingTestSuite.swift",
    "content": "//\n//  DateFormattingTestSuite.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 28..\n//\n\nimport Foundation\nimport Testing\nimport ToucanSerialization\n@testable import ToucanSource\n\n@Suite\nstruct DateFormattingTestSuite {\n    @Test\n    func decodeFullSpec() throws {\n        let yaml = \"\"\"\n            locale: \"fr_FR\"\n            timeZone: \"Europe/Budapest\"\n            format: \"yyyy-MM-dd\"\n            \"\"\"\n        let decoder = ToucanYAMLDecoder()\n        let options = try decoder.decode(\n            DateFormatterConfig.self,\n            from: yaml\n        )\n        #expect(options.localization.locale == \"fr_FR\")\n        #expect(options.localization.timeZone == \"Europe/Budapest\")\n        #expect(options.format == \"yyyy-MM-dd\")\n    }\n\n    @Test\n    func decodeDefaultValues() throws {\n        let yaml = \"\"\"\n            format: \"MM/dd/yyyy\"\n            \"\"\"\n        let decoder = ToucanYAMLDecoder()\n        let options = try decoder.decode(\n            DateFormatterConfig.self,\n            from: yaml\n        )\n        #expect(\n            options.localization.locale\n                == DateLocalization.defaults.locale\n        )\n        #expect(\n            options.localization.timeZone\n                == DateLocalization.defaults.timeZone\n        )\n        #expect(options.format == \"MM/dd/yyyy\")\n    }\n\n    @Test\n    func encodeProducesExpectedYAML() throws {\n        let options = DateFormatterConfig(\n            localization: DateLocalization(\n                locale: \"de_DE\",\n                timeZone: \"Europe/Berlin\"\n            ),\n            format: \"dd.MM.yyyy\"\n        )\n        let encoder = ToucanYAMLEncoder()\n        let yamlString: String = try encoder.encode(options)\n        let exp = \"\"\"\n            format: dd.MM.yyyy\n            locale: de_DE\n            timeZone: Europe/Berlin\n            \"\"\"\n            .trimmingCharacters(in: .whitespacesAndNewlines)\n        #expect(\n            yamlString.trimmingCharacters(in: .whitespacesAndNewlines) == exp\n        )\n    }\n\n    @Test\n    func encodeDefaultsProducesYAMLWithFormatOnly() throws {\n        let options = DateFormatterConfig(\n            localization: DateLocalization.defaults,\n            format: \"yyyy\"\n        )\n        let encoder = ToucanYAMLEncoder()\n        let yamlString: String = try encoder.encode(options)\n\n        let exp = \"\"\"\n            format: yyyy\n            \"\"\"\n            .trimmingCharacters(in: .whitespacesAndNewlines)\n\n        #expect(\n            yamlString.trimmingCharacters(in: .whitespacesAndNewlines) == exp\n        )\n    }\n\n    @Test\n    func invalidLocale() throws {\n        let decoder = ToucanYAMLDecoder()\n        let yaml = \"\"\"\n                format: yyyy\n                locale: invalid\n                timeZone: GMT\n            \"\"\"\n\n        do {\n            _ = try decoder.decode(DateLocalization.self, from: yaml)\n        }\n        catch {\n            if let context = error.lookup({\n                if case let DecodingError.dataCorrupted(ctx) = $0 {\n                    return ctx\n                }\n                return nil\n            }) {\n                let expected = \"Invalid locale identifier.\"\n                #expect(context.debugDescription == expected)\n            }\n            else {\n                throw error\n            }\n        }\n    }\n\n    @Test\n    func invalidTimeZone() throws {\n        let decoder = ToucanYAMLDecoder()\n        let yaml = \"\"\"\n                format: yyyy\n                locale: en-US\n                timeZone: invalid\n            \"\"\"\n\n        do {\n            _ = try decoder.decode(DateLocalization.self, from: yaml)\n        }\n        catch {\n            if let context = error.lookup({\n                if case let DecodingError.dataCorrupted(ctx) = $0 {\n                    return ctx\n                }\n                return nil\n            }) {\n                let expected = \"Invalid time zone identifier.\"\n                #expect(context.debugDescription == expected)\n            }\n            else {\n                throw error\n            }\n        }\n    }\n\n    @Test\n    func invalidFormat() throws {\n        let decoder = ToucanYAMLDecoder()\n        let yaml = \"\"\"\n                format: \"\"\n                locale: en-US\n                timeZone: GMT\n            \"\"\"\n\n        do {\n            _ = try decoder.decode(DateFormatterConfig.self, from: yaml)\n        }\n        catch {\n            if let context = error.lookup({\n                if case let DecodingError.dataCorrupted(ctx) = $0 {\n                    return ctx\n                }\n                return nil\n            }) {\n                let expected = \"Empty date format value.\"\n                #expect(context.debugDescription == expected)\n            }\n            else {\n                throw error\n            }\n        }\n    }\n\n    @Test\n    func preserveDefaultValues() throws {\n        let original = DateFormatterConfig(\n            localization: DateLocalization(\n                locale: \"en-US\",\n                timeZone: \"GMT\"\n            ),\n            format: \"yyyy-MM-dd\"\n        )\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n        let yamlString: String = try encoder.encode(original)\n        let decoded = try decoder.decode(\n            DateFormatterConfig.self,\n            from: yamlString\n        )\n        #expect(decoded == original)\n    }\n\n    @Test\n    func preserveCustomValues() throws {\n        let original = DateFormatterConfig(\n            localization: DateLocalization(\n                locale: \"hu-HU\",\n                timeZone: \"CET\"\n            ),\n            format: \"yyyy-MM-dd\"\n        )\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n        let yamlString: String = try encoder.encode(original)\n        let decoded = try decoder.decode(\n            DateFormatterConfig.self,\n            from: yamlString\n        )\n        #expect(decoded == original)\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSourceTests/Objects/Pipeline/PipelineContentTypeTestSuite.swift",
    "content": "//\n//  PipelineContentTypeTestSuite.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 03. 12..\n\nimport Foundation\nimport Testing\nimport ToucanSerialization\n\n@testable import ToucanSource\n\n@Suite\nstruct PipelineContentTypeTestSuite {\n    @Test\n    func invalidKey() throws {\n        let data = \"\"\"\n            foo: bar\n            \"\"\"\n            .data(using: .utf8)!\n\n        let decoder = ToucanYAMLDecoder()\n\n        do {\n            let _ = try decoder.decode(Pipeline.ContentTypes.self, from: data)\n        }\n        catch {\n            if let context = error.lookup({\n                if case let DecodingError.dataCorrupted(ctx) = $0 {\n                    return ctx\n                }\n                return nil\n            }) {\n                let expected =\n                    \"Unknown keys found: `foo`. Expected keys: `exclude`, `filterRules`, `include`, `lastUpdate`.\"\n                #expect(context.debugDescription == expected)\n            }\n            else {\n                throw error\n            }\n        }\n    }\n\n    @Test\n    func standard() throws {\n        let data = \"\"\"\n            include:\n                - post\n            exclude:\n                - rss\n            lastUpdate:\n                - page\n            \"\"\"\n            .data(using: .utf8)!\n\n        let decoder = ToucanYAMLDecoder()\n\n        let result = try decoder.decode(\n            Pipeline.ContentTypes.self,\n            from: data\n        )\n\n        #expect(result.include == [\"post\"])\n        #expect(result.exclude == [\"rss\"])\n        #expect(result.lastUpdate == [\"page\"])\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSourceTests/Objects/Pipeline/PipelineScopeContextTestSuite.swift",
    "content": "//\n//  PipelineScopeContextTestSuite.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 03. 12..\n\nimport Foundation\nimport Testing\nimport ToucanSerialization\n\n@testable import ToucanSource\n\n@Suite\nstruct PipelineScopeContextTestSuite {\n    @Test\n    func initialization() throws {\n        let result = Pipeline.Scope.Context(stringValue: \"foo\")\n\n        #expect(!result.contains(.properties))\n        #expect(!result.contains(.contents))\n        #expect(!result.contains(.relations))\n        #expect(!result.contains(.queries))\n        #expect(!result.contains(.detail))\n    }\n\n    @Test\n    func decodingMultipleValues() throws {\n        let json =\n            #\"[\"\\#(Pipeline.Scope.Context.Keys.contents.rawValue)\", \"\\#(Pipeline.Scope.Context.Keys.queries.rawValue)\"]\"#\n        let data = json.data(using: .utf8)!\n        let result = try ToucanJSONDecoder()\n            .decode(Pipeline.Scope.Context.self, from: data)\n\n        #expect(!result.contains(.properties))\n        #expect(result.contains(.contents))\n        #expect(!result.contains(.relations))\n        #expect(result.contains(.queries))\n        #expect(!result.contains(.detail))\n    }\n\n    @Test\n    func decodingSingleValue() throws {\n        let json = #\"\"\\#(Pipeline.Scope.Context.Keys.properties.rawValue)\"\"#\n        let data = json.data(using: .utf8)!\n        let result = try ToucanJSONDecoder()\n            .decode(Pipeline.Scope.Context.self, from: data)\n\n        #expect(result.contains(.properties))\n        #expect(!result.contains(.contents))\n        #expect(!result.contains(.relations))\n        #expect(!result.contains(.queries))\n        #expect(!result.contains(.detail))\n    }\n\n    @Test\n    func decodingSingleAllValue() throws {\n        let json = #\"\"\\#(Pipeline.Scope.Context.Keys.detail.rawValue)\"\"#\n        let data = json.data(using: .utf8)!\n        let result = try ToucanJSONDecoder()\n            .decode(Pipeline.Scope.Context.self, from: data)\n\n        #expect(result.contains(.properties))\n        #expect(result.contains(.contents))\n        #expect(result.contains(.relations))\n        #expect(result.contains(.queries))\n        #expect(result.contains(.detail))\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSourceTests/Objects/Pipeline/PipelineScopeTestSuite.swift",
    "content": "//\n//  PipelineScopeTestSuite.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 03. 12..\n\nimport Foundation\nimport Testing\nimport ToucanSerialization\n\n@testable import ToucanSource\n\n@Suite\nstruct PipelineScopeTestSuite {\n\n    @Test\n    func minimal() throws {\n        let data = \"\"\"\n            context: detail\n            \"\"\"\n            .data(using: .utf8)!\n\n        let decoder = ToucanYAMLDecoder()\n\n        let result = try decoder.decode(\n            Pipeline.Scope.self,\n            from: data\n        )\n\n        #expect(result.context == .detail)\n        try #require(result.fields.count == 0)\n        #expect(result.fields == [])\n    }\n\n    @Test\n    func fields() throws {\n        let data = \"\"\"\n            context: properties\n            fields: \n                - foo\n                - bar\n            \"\"\"\n            .data(using: .utf8)!\n\n        let decoder = ToucanYAMLDecoder()\n\n        let result = try decoder.decode(\n            Pipeline.Scope.self,\n            from: data\n        )\n\n        #expect(result.context == .properties)\n        try #require(result.fields.count == 2)\n        #expect(result.fields == [\"foo\", \"bar\"])\n    }\n\n    @Test\n    func context() throws {\n        let data = \"\"\"\n            context: \n                - contents\n                - relations\n            fields: \n                - foo\n            \"\"\"\n            .data(using: .utf8)!\n\n        let decoder = ToucanYAMLDecoder()\n\n        let result = try decoder.decode(\n            Pipeline.Scope.self,\n            from: data\n        )\n\n        #expect(result.context == [.contents, .relations])\n        try #require(result.fields.count == 1)\n        #expect(result.fields == [\"foo\"])\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSourceTests/Objects/Pipeline/PipelineTestSuite.swift",
    "content": "//\n//  PipelineTestSuite.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 03. 30..\n\nimport Foundation\nimport Testing\nimport ToucanSerialization\n\n@testable import ToucanSource\n\n@Suite\nstruct PipelineTestSuite {\n\n    @Test\n    func minimal() throws {\n        let data = \"\"\"\n            id: test\n            engine: \n                id: engine\n            output:\n                path: path\n                file: file\n                ext: ext\n            \"\"\"\n            .data(using: .utf8)!\n\n        let decoder = ToucanYAMLDecoder()\n\n        let result = try decoder.decode(\n            Pipeline.self,\n            from: data\n        )\n\n        #expect(result.id == \"test\")\n        #expect(result.engine.id == \"engine\")\n        #expect(result.output.path == \"path\")\n        #expect(result.output.file == \"file\")\n        #expect(result.output.ext == \"ext\")\n    }\n\n    @Test\n    func standard() throws {\n        let data = \"\"\"\n            id: test\n            queries: \n                featured:\n                    contentType: post\n                    limit: 10\n                    filter:\n                        key: featured\n                        operator: equals\n                        value: true\n                    orderBy:\n                        - key: publication\n                          direction: desc\n\n            contentTypes: \n                include:\n                    - page\n                    - post\n            engine: \n                id: test\n                options:\n                    foo: bar\n                    foo2: \n                    bool: false\n                    double: 2.0\n                    int: 100\n                    date: 01/16/2023\n                    array:\n                        - value1\n                        - value2\n            output:\n                path: \"{{slug}}\"\n                file: \"{{id}}\"\n                ext: json\n            \"\"\"\n            .data(using: .utf8)!\n\n        let decoder = ToucanYAMLDecoder()\n\n        let result = try decoder.decode(\n            Pipeline.self,\n            from: data\n        )\n\n        #expect(result.contentTypes.include == [\"page\", \"post\"])\n        let query = try #require(result.queries[\"featured\"])\n        #expect(query.contentType == \"post\")\n        #expect(result.engine.id == \"test\")\n        #expect(result.engine.options.string(\"\") == nil)\n        #expect(result.engine.options.string(\"foo\") == \"bar\")\n        #expect(result.engine.options.string(\"foo.foo2\") == nil)\n        #expect(\n            result.engine.options.string(\"foo4\", allowingEmptyValue: true)\n                == nil\n        )\n        #expect(result.engine.options.string(\"foo4\") == nil)\n        #expect(result.engine.options.bool(\"bool\") == false)\n        #expect(result.engine.options.double(\"double\") == 2.0)\n        #expect(result.engine.options.int(\"int\") == 100)\n\n        let formatter = DateFormatter()\n        formatter.locale = .init(identifier: \"en-US\")\n        formatter.timeZone = .init(secondsFromGMT: 0)!\n\n        formatter.dateFormat = \"MM/dd/yyyy\"\n        #expect(\n            result.engine.options.date(\"date\", formatter: formatter)?\n                .formatted(.iso8601) == \"2023-01-16T00:00:00Z\"\n        )\n        #expect(\n            result.engine.options.date(\"date2\", formatter: formatter)?\n                .formatted() == nil\n        )\n        #expect(\n            result.engine.options.array(\"array\", as: String.self) == [\n                \"value1\", \"value2\",\n            ]\n        )\n        #expect(result.engine.options.array(\"array\", as: Int.self) == [])\n    }\n\n    @Test\n    func scopes() throws {\n        let data = \"\"\"\n            id: test\n            scopes: \n                post:\n                    list:\n                        context: \n                            - detail\n                        fields:\n            engine: \n                id: test\n            output:\n                path: \"{{slug}}\"\n                file: index\n                ext: html\n            \"\"\"\n            .data(using: .utf8)!\n\n        let decoder = ToucanYAMLDecoder()\n\n        let result = try decoder.decode(\n            Pipeline.self,\n            from: data\n        )\n\n        #expect(result.contentTypes.include.isEmpty)\n        #expect(result.engine.id == \"test\")\n        let defaultScope = try #require(\n            result.scopes[Pipeline.Scope.Keys.wildcard.rawValue]\n        )\n        let defaultReferenceScope = try #require(\n            defaultScope[Pipeline.Scope.Context.Keys.reference.rawValue]\n        )\n        let defaultListScope = try #require(\n            defaultScope[Pipeline.Scope.Context.Keys.list.rawValue]\n        )\n        let defaultDetailScope = try #require(\n            defaultScope[Pipeline.Scope.Context.Keys.detail.rawValue]\n        )\n        #expect(defaultReferenceScope.context == .reference)\n        #expect(defaultListScope.context == .list)\n        #expect(defaultDetailScope.context == .detail)\n        let postScope = try #require(result.scopes[\"post\"])\n        let postListScope = try #require(\n            postScope[Pipeline.Scope.Keys.list.rawValue]\n        )\n        #expect(postListScope.context == .detail)\n    }\n\n    @Test\n    func assets() throws {\n        let data = \"\"\"\n            id: test\n            assets:\n                properties:\n                    - action: add\n                      property: js\n                      resolvePath: false\n                      input:\n                        name: main\n                        ext: js\n                    - action: set\n                      property: image\n                      resolvePath: true\n                      input:\n                        name: cover\n                        ext: jpg\n                    - action: load\n                      property: svgs\n                      resolvePath: false\n                      input:\n                        name: \"*\"\n                        ext: svg\n                    - action: parse\n                      property: data\n                      resolvePath: false\n                      input:\n                        name: \"*\"\n                        ext: json\n            engine: \n                id: engine\n            output:\n                path: path\n                file: file\n                ext: ext\n            \"\"\"\n            .data(using: .utf8)!\n\n        let decoder = ToucanYAMLDecoder()\n\n        let result = try decoder.decode(\n            Pipeline.self,\n            from: data\n        )\n\n        #expect(result.assets.behaviors.isEmpty)\n        #expect(result.assets.properties.count == 4)\n        #expect(result.assets.properties[0].action == .add)\n        #expect(result.assets.properties[1].action == .set)\n        #expect(result.assets.properties[2].action == .load)\n        #expect(result.assets.properties[3].action == .parse)\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSourceTests/Objects/Pipeline/PipelineTransformersTestSuite.swift",
    "content": "//\n//  PipelineTransformersTestSuite.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 18..\n//\n\nimport Foundation\nimport Testing\nimport ToucanSerialization\n\n@testable import ToucanSource\n\n@Suite\nstruct PipelineTransformersTestSuite {\n    @Test\n    func initWithName() throws {\n        let contentTransformer = Pipeline.Transformers.Transformer(\n            name: \"test\"\n        )\n        #expect(contentTransformer.name == \"test\")\n    }\n\n    @Test\n    func initWithRun() throws {\n        let transformerPipeline = Pipeline.Transformers(\n            run: [\n                .init(name: \"test\")\n            ],\n            isMarkdownResult: false\n        )\n        #expect(transformerPipeline.isMarkdownResult == false)\n        #expect(transformerPipeline.run[0].name == \"test\")\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSourceTests/Objects/Property/PropertyTestSuite.swift",
    "content": "//\n//  PropertyTestSuite.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 03. 30..\n\nimport Foundation\nimport Testing\nimport ToucanSerialization\n\n@testable import ToucanSource\n\n@Suite\nstruct PropertyTestSuite {\n\n    @Test\n    func stringType() throws {\n        let data = \"\"\"\n            defaultValue: hello\n            required: false\n            type: string\n            \"\"\"\n\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(Property.self, from: data)\n        let encoder = ToucanYAMLEncoder()\n        let yaml = try encoder.encode(result)\n\n        #expect(result.type == .string)\n        #expect(result.required == false)\n        #expect(result.defaultValue?.value as? String == \"hello\")\n\n        #expect(\n            data.trimmingCharacters(in: .whitespacesAndNewlines)\n                == yaml.trimmingCharacters(in: .whitespacesAndNewlines)\n        )\n    }\n\n    @Test\n    func assetType() throws {\n        let data = \"\"\"\n            required: true\n            type: asset\n            \"\"\"\n\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(Property.self, from: data)\n        let encoder = ToucanYAMLEncoder()\n        let yaml = try encoder.encode(result)\n\n        #expect(result.type == .asset)\n        #expect(result.required == true)\n\n        #expect(\n            data.trimmingCharacters(in: .whitespacesAndNewlines)\n                == yaml.trimmingCharacters(in: .whitespacesAndNewlines)\n        )\n    }\n\n    @Test\n    func dateTypeWithFormat() throws {\n        let data = \"\"\"\n            type: date\n            config: \n                format: \"ymd\"\n                locale: en-US\n                timeZone: EST\n            \"\"\"\n\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(Property.self, from: data)\n\n        #expect(\n            result.type\n                == .date(\n                    config: .init(\n                        localization: .init(\n                            locale: \"en-US\",\n                            timeZone: \"EST\"\n                        ),\n                        format: \"ymd\"\n                    )\n                )\n        )\n        #expect(result.required == true)\n        #expect(result.defaultValue == nil)\n    }\n\n    @Test\n    func dateTypeWithoutFormat() throws {\n        let data = \"\"\"\n            type: date\n            required: true\n            \"\"\"\n\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(Property.self, from: data)\n\n        #expect(result.type == .date(config: nil))\n        #expect(result.required == true)\n        #expect(result.defaultValue == nil)\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSourceTests/Objects/Property/PropertyTypeTestSuite.swift",
    "content": "//\n//  PropertyTypeTestSuite.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 01. 31..\n//\n\nimport Foundation\nimport Testing\nimport ToucanSerialization\n\n@testable import ToucanSource\n\n@Suite\nstruct PropertyTypeTestSuite {\n    @Test\n    func equality() throws {\n        let dateFormat = PropertyType.date(config: nil)\n        #expect(PropertyType.bool == .bool)\n        #expect(PropertyType.bool != .int)\n        #expect(PropertyType.int == .int)\n        #expect(PropertyType.int != .double)\n        #expect(PropertyType.double == .double)\n        #expect(PropertyType.double != .string)\n        #expect(PropertyType.string == .string)\n        #expect(PropertyType.string != dateFormat)\n        #expect(dateFormat == .date(config: nil))\n        #expect(\n            dateFormat\n                != .date(\n                    config: .init(\n                        localization: .defaults,\n                        format: \"y.m.d\"\n                    )\n                )\n        )\n    }\n\n    @Test\n    func decodingBool() throws {\n        let object = \"\"\"\n            type: bool\n            \"\"\"\n\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(PropertyType.self, from: object)\n\n        #expect(result == .bool)\n    }\n\n    @Test\n    func encodingBool() throws {\n        let object = \"\"\"\n            type: bool\n            \"\"\"\n\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(PropertyType.self, from: object)\n        let encoder = ToucanYAMLEncoder()\n        let yaml = try encoder.encode(result)\n\n        #expect(result == .bool)\n        #expect(\n            object.trimmingCharacters(in: .whitespacesAndNewlines)\n                == yaml.trimmingCharacters(in: .whitespacesAndNewlines)\n        )\n    }\n\n    @Test\n    func decodingInt() throws {\n        let object = \"\"\"\n            type: int\n            \"\"\"\n\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(PropertyType.self, from: object)\n\n        #expect(result == .int)\n    }\n\n    @Test\n    func decodingDouble() throws {\n        let object = \"\"\"\n            type: double\n            \"\"\"\n\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(PropertyType.self, from: object)\n\n        #expect(result == .double)\n    }\n\n    @Test\n    func decodingString() throws {\n        let object = \"\"\"\n            type: string\n            \"\"\"\n\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(PropertyType.self, from: object)\n\n        #expect(result == .string)\n    }\n\n    @Test\n    func decodingDate() throws {\n        let object = \"\"\"\n            type: date\n            config:\n                format: \"y.m.d\"\n            \"\"\"\n\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(PropertyType.self, from: object)\n\n        #expect(\n            result\n                == .date(\n                    config: .init(\n                        localization: .defaults,\n                        format: \"y.m.d\"\n                    )\n                )\n        )\n    }\n\n    @Test\n    func decodingArrayOfBool() throws {\n        let object = \"\"\"\n            type: array\n            of: \n                type: bool\n            \"\"\"\n\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(PropertyType.self, from: object)\n\n        #expect(result == .array(of: .bool))\n    }\n\n    @Test\n    func decodingArrayOfInt() throws {\n        let object = \"\"\"\n            type: array\n            of: \n                type: int\n            \"\"\"\n\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(PropertyType.self, from: object)\n\n        #expect(result == .array(of: .int))\n    }\n\n    @Test\n    func decodingArrayOfDouble() throws {\n        let object = \"\"\"\n            type: array\n            of: \n                type: double\n            \"\"\"\n\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(PropertyType.self, from: object)\n\n        #expect(result == .array(of: .double))\n    }\n\n    @Test\n    func decodingArrayOfString() throws {\n        let object = \"\"\"\n            type: array\n            of: \n                type: string\n            \"\"\"\n\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(PropertyType.self, from: object)\n\n        #expect(result == .array(of: .string))\n    }\n\n    @Test\n    func decodingArrayOfDate() throws {\n        let object = \"\"\"\n            type: array\n            of: \n                type: date\n                config:\n                    format: \"y.m.d\"\n            \"\"\"\n\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(PropertyType.self, from: object)\n\n        #expect(\n            result\n                == .array(\n                    of: .date(\n                        config: .init(\n                            localization: .defaults,\n                            format: \"y.m.d\"\n                        )\n                    )\n                )\n        )\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSourceTests/Objects/Query/ConditionTestSuite.swift",
    "content": "//\n//  ConditionTestSuite.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 18..\n//\n\nimport Foundation\nimport Testing\nimport ToucanSerialization\n\n@testable import ToucanSource\n\n@Suite\nstruct ConditionTestSuite {\n    @Test\n    func fieldBasics() throws {\n        let object = Condition.field(\n            key: \"foo\",\n            operator: .equals,\n            value: \"a\"\n        )\n\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n\n        let value1: String = try encoder.encode(object)\n        let result1 = try decoder.decode(Condition.self, from: value1)\n\n        let value2: Data = try encoder.encode(object)\n        let result2 = try decoder.decode(Condition.self, from: value2)\n\n        #expect(object == result1)\n        #expect(object == result2)\n    }\n\n    @Test\n    func andBasics() throws {\n        let object = Condition.and(\n            [\n                .field(\n                    key: \"foo\",\n                    operator: .equals,\n                    value: \"a\"\n                ),\n                .field(\n                    key: \"bar\",\n                    operator: .notEquals,\n                    value: \"b\"\n                ),\n            ]\n        )\n\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n\n        let value1: String = try encoder.encode(object)\n        let result1 = try decoder.decode(Condition.self, from: value1)\n\n        let value2: Data = try encoder.encode(object)\n        let result2 = try decoder.decode(Condition.self, from: value2)\n\n        #expect(object == result1)\n        #expect(object == result2)\n    }\n\n    @Test\n    func orBasics() throws {\n        let object = Condition.or(\n            [\n                .field(\n                    key: \"foo\",\n                    operator: .equals,\n                    value: \"a\"\n                ),\n                .field(\n                    key: \"bar\",\n                    operator: .notEquals,\n                    value: \"b\"\n                ),\n            ]\n        )\n\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n\n        let value1: String = try encoder.encode(object)\n        let result1 = try decoder.decode(Condition.self, from: value1)\n\n        let value2: Data = try encoder.encode(object)\n        let result2 = try decoder.decode(Condition.self, from: value2)\n\n        #expect(object == result1)\n        #expect(object == result2)\n    }\n\n    @Test\n    func customField() throws {\n        let value = \"\"\"\n            key: foo\n            operator: equals\n            value: a\n            \"\"\" + \"\\n\"\n\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(Condition.self, from: value)\n\n        let expectation = Condition.field(\n            key: \"foo\",\n            operator: .equals,\n            value: \"a\"\n        )\n\n        let encodedValue: String = try encoder.encode(expectation)\n\n        #expect(result == expectation)\n        #expect(value == encodedValue)\n    }\n\n    @Test\n    func customAnd() throws {\n        let value = \"\"\"\n            and:\n            - key: foo\n              operator: equals\n              value: a\n            - key: bar\n              operator: notEquals\n              value: b\n            \"\"\" + \"\\n\"\n\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(Condition.self, from: value)\n\n        let expectation = Condition.and(\n            [\n                .field(\n                    key: \"foo\",\n                    operator: .equals,\n                    value: \"a\"\n                ),\n                .field(\n                    key: \"bar\",\n                    operator: .notEquals,\n                    value: \"b\"\n                ),\n            ]\n        )\n\n        let encodedValue: String = try encoder.encode(expectation)\n\n        #expect(result == expectation)\n        #expect(value == encodedValue)\n    }\n\n    @Test\n    func customOr() throws {\n        let value = \"\"\"\n            or:\n            - key: foo\n              operator: equals\n              value: a\n            - key: bar\n              operator: notEquals\n              value: b\n            \"\"\" + \"\\n\"\n\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(Condition.self, from: value)\n\n        let expectation = Condition.or(\n            [\n                .field(\n                    key: \"foo\",\n                    operator: .equals,\n                    value: \"a\"\n                ),\n                .field(\n                    key: \"bar\",\n                    operator: .notEquals,\n                    value: \"b\"\n                ),\n            ]\n        )\n\n        let encodedValue: String = try encoder.encode(expectation)\n\n        #expect(result == expectation)\n        #expect(value == encodedValue)\n    }\n\n    @Test\n    func stringValue() throws {\n        let data = \"\"\"\n            key: name\n            operator: equals\n            value: hello\n            \"\"\"\n\n        let decoder = ToucanYAMLDecoder()\n\n        let result = try decoder.decode(\n            Condition.self,\n            from: data\n        )\n\n        guard case let .field(key, op, value) = result else {\n            Issue.record(\"Result is not a field case.\")\n            return\n        }\n\n        #expect(key == \"name\")\n        #expect(op == .equals)\n        #expect(value.value(as: String.self) == \"hello\")\n    }\n\n    @Test\n    func arrayValue() throws {\n        let data = \"\"\"\n            key: name\n            operator: in\n            value: \n                - foo\n                - bar\n                - baz\n            \"\"\"\n\n        let decoder = ToucanYAMLDecoder()\n\n        let result = try decoder.decode(\n            Condition.self,\n            from: data\n        )\n\n        guard case let .field(key, op, value) = result else {\n            Issue.record(\"Result is not a field case.\")\n            return\n        }\n\n        #expect(key == \"name\")\n        #expect(op == .in)\n        #expect(value.value(as: [String].self) == [\"foo\", \"bar\", \"baz\"])\n    }\n\n    @Test\n    func orConditionValues() throws {\n        let data = \"\"\"\n            or:\n                - key: name\n                  operator: equals\n                  value: hello\n\n                - key: description\n                  operator: like\n                  value: foo\n            \"\"\"\n\n        let decoder = ToucanYAMLDecoder()\n\n        let result = try decoder.decode(\n            Condition.self,\n            from: data\n        )\n\n        guard case let .or(conditions) = result else {\n            Issue.record(\"Result is not an and case.\")\n            return\n        }\n\n        try #require(conditions.count == 2)\n\n        guard case let .field(key, op, value) = conditions[0] else {\n            Issue.record(\"Condition is not a field case.\")\n            return\n        }\n\n        #expect(key == \"name\")\n        #expect(op == .equals)\n        #expect(value.value(as: String.self) == \"hello\")\n\n        guard case let .field(key, op, value) = conditions[1] else {\n            Issue.record(\"Condition is not a field case.\")\n            return\n        }\n\n        #expect(key == \"description\")\n        #expect(op == .like)\n        #expect(value.value(as: String.self) == \"foo\")\n    }\n\n    @Test\n    func complexCondition() throws {\n        let data = \"\"\"\n            or:\n                - key: name\n                  operator: equals\n                  value: hello\n\n                - and: \n                    - key: featured\n                      operator: equals\n                      value: false\n\n                    - key: likes\n                      operator: greaterThan\n                      value: 100\n            \"\"\"\n\n        let decoder = ToucanYAMLDecoder()\n\n        let result = try decoder.decode(\n            Condition.self,\n            from: data\n        )\n\n        guard case let .or(conditions) = result else {\n            Issue.record(\"Result is not an and case.\")\n            return\n        }\n\n        try #require(conditions.count == 2)\n\n        guard case let .field(key, op, value) = conditions[0] else {\n            Issue.record(\"Condition is not a field case.\")\n            return\n        }\n\n        #expect(key == \"name\")\n        #expect(op == .equals)\n        #expect(value.value(as: String.self) == \"hello\")\n\n        guard case let .and(subconditions) = conditions[1] else {\n            Issue.record(\"Result is not an and case.\")\n            return\n        }\n\n        guard case let .field(key, op, value) = subconditions[0] else {\n            Issue.record(\"Condition is not a field case.\")\n            return\n        }\n\n        #expect(key == \"featured\")\n        #expect(op == .equals)\n        #expect(value.value(as: Bool.self) == false)\n\n        guard case let .field(key, op, value) = subconditions[1] else {\n            Issue.record(\"Condition is not a field case.\")\n            return\n        }\n\n        #expect(key == \"likes\")\n        #expect(op == .greaterThan)\n        #expect(value.value(as: Int.self) == 100)\n    }\n\n    @Test\n    func wrongCondition() throws {\n        let data = \"\"\"\n            wrong:\n                - key: name\n                  operator: equals\n                  value: hello\n            \"\"\"\n\n        let decoder = ToucanYAMLDecoder()\n        do {\n            _ = try decoder.decode(\n                Condition.self,\n                from: data\n            )\n        }\n        catch {\n            #expect(\n                error.localizedDescription.contains(\n                    \"ToucanDecoderError\"\n                )\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSourceTests/Objects/Query/DirectionTestSuite.swift",
    "content": "//\n//  DirectionTestSuite.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 18..\n//\n\nimport Foundation\nimport Testing\nimport ToucanSerialization\n\n@testable import ToucanSource\n\n@Suite\nstruct DirectionTestSuite {\n    @Test\n    func basics() throws {\n        let object = Direction.allCases\n\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n\n        let value1: String = try encoder.encode(object)\n        let result1 = try decoder.decode([Direction].self, from: value1)\n\n        let value2: Data = try encoder.encode(object)\n        let result2 = try decoder.decode([Direction].self, from: value2)\n\n        #expect(object == result1)\n        #expect(object == result2)\n    }\n\n    @Test\n    func defaults() throws {\n        let object = Direction.defaults\n        #expect(object == .asc)\n    }\n\n    @Test\n    func ascending() throws {\n        let value = \"asc\"\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(Direction.self, from: value)\n        let expectation = Direction.asc\n\n        #expect(result == expectation)\n    }\n\n    @Test\n    func descending() throws {\n        let value = \"desc\"\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(Direction.self, from: value)\n        let expectation = Direction.desc\n\n        #expect(result == expectation)\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSourceTests/Objects/Query/OperatorTestSuite.swift",
    "content": "//\n//  OperatorTestSuite.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 18..\n//\n\nimport Foundation\nimport Testing\nimport ToucanSerialization\n\n@testable import ToucanSource\n\n@Suite\nstruct OperatorTestSuite {\n    @Test\n    func basics() throws {\n        let object = Operator.allCases\n\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n\n        let value1: String = try encoder.encode(object)\n        let result1 = try decoder.decode([Operator].self, from: value1)\n\n        let value2: Data = try encoder.encode(object)\n        let result2 = try decoder.decode([Operator].self, from: value2)\n\n        #expect(object == result1)\n        #expect(object == result2)\n    }\n\n    @Test\n    func equals() throws {\n        let value = \"equals\"\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(Operator.self, from: value)\n        let expectation = Operator.equals\n\n        #expect(result == expectation)\n    }\n\n    @Test\n    func notEquals() throws {\n        let value = \"notEquals\"\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(Operator.self, from: value)\n        let expectation = Operator.notEquals\n\n        #expect(result == expectation)\n    }\n\n    @Test\n    func lessThan() throws {\n        let value = \"lessThan\"\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(Operator.self, from: value)\n        let expectation = Operator.lessThan\n\n        #expect(result == expectation)\n    }\n\n    @Test\n    func lessThanOrEquals() throws {\n        let value = \"lessThanOrEquals\"\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(Operator.self, from: value)\n        let expectation = Operator.lessThanOrEquals\n\n        #expect(result == expectation)\n    }\n\n    @Test\n    func greaterThan() throws {\n        let value = \"greaterThan\"\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(Operator.self, from: value)\n        let expectation = Operator.greaterThan\n\n        #expect(result == expectation)\n    }\n\n    @Test\n    func greaterThanOrEquals() throws {\n        let value = \"greaterThanOrEquals\"\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(Operator.self, from: value)\n        let expectation = Operator.greaterThanOrEquals\n\n        #expect(result == expectation)\n    }\n\n    @Test\n    func like() throws {\n        let value = \"like\"\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(Operator.self, from: value)\n        let expectation = Operator.like\n\n        #expect(result == expectation)\n    }\n\n    @Test\n    func caseInsensitiveLike() throws {\n        let value = \"caseInsensitiveLike\"\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(Operator.self, from: value)\n        let expectation = Operator.caseInsensitiveLike\n\n        #expect(result == expectation)\n    }\n\n    @Test\n    func `in`() throws {\n        let value = \"in\"\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(Operator.self, from: value)\n        let expectation = Operator.in\n\n        #expect(result == expectation)\n    }\n\n    @Test\n    func contains() throws {\n        let value = \"contains\"\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(Operator.self, from: value)\n        let expectation = Operator.contains\n\n        #expect(result == expectation)\n    }\n\n    @Test\n    func matching() throws {\n        let value = \"matching\"\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(Operator.self, from: value)\n        let expectation = Operator.matching\n\n        #expect(result == expectation)\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSourceTests/Objects/Query/OrderTestSuite.swift",
    "content": "//\n//  OrderTestSuite.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 18..\n//\n\nimport Foundation\nimport Testing\nimport ToucanSerialization\n\n@testable import ToucanSource\n\n@Suite\nstruct OrderTestSuite {\n    @Test\n    func basics() throws {\n        let object = Order(key: \"foo\")\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n\n        let value1: String = try encoder.encode(object)\n        let result1 = try decoder.decode(Order.self, from: value1)\n\n        let value2: Data = try encoder.encode(object)\n        let result2 = try decoder.decode(Order.self, from: value2)\n\n        #expect(object == result1)\n        #expect(object == result2)\n    }\n\n    @Test\n    func defaults() throws {\n        let value = \"\"\"\n            key: foo\n            \"\"\"\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(Order.self, from: value)\n        let expectation = Order(key: \"foo\")\n\n        #expect(result == expectation)\n    }\n\n    @Test\n    func custom() throws {\n        let value = \"\"\"\n            direction: desc\n            key: foo\n            \"\"\" + \"\\n\"\n\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(Order.self, from: value)\n\n        let expectation = Order(key: \"foo\", direction: .desc)\n        let encodedValue: String = try encoder.encode(expectation)\n\n        #expect(result == expectation)\n        #expect(value == encodedValue)\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSourceTests/Objects/Query/QueryTestSuite.swift",
    "content": "//\n//  QueryTestSuite.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 04. 15..\n\nimport Foundation\nimport Testing\nimport ToucanSerialization\n\n@testable import ToucanSource\n\n@Suite\nstruct QueryTestSuite {\n    @Test\n    func basics() throws {\n        let object = Query(\n            contentType: \"post\",\n            scope: \"custom\",\n            limit: 10,\n            offset: 5,\n            filter: .field(key: \"title\", operator: .like, value: \"foo\"),\n            orderBy: [\n                .init(key: \"publication\", direction: .desc),\n                .init(key: \"title\"),\n            ]\n        )\n\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n\n        let value1: String = try encoder.encode(object)\n        let result1 = try decoder.decode(Query.self, from: value1)\n\n        let value2: Data = try encoder.encode(object)\n        let result2 = try decoder.decode(Query.self, from: value2)\n\n        #expect(object == result1)\n        #expect(object == result2)\n    }\n\n    @Test\n    func defaults() throws {\n        let data = \"\"\"\n            contentType: post\n            \"\"\"\n\n        let decoder = ToucanYAMLDecoder()\n\n        let result = try decoder.decode(Query.self, from: data)\n\n        #expect(result.contentType == \"post\")\n        #expect(result.scope == nil)\n        #expect(result.limit == nil)\n        #expect(result.offset == nil)\n        #expect(result.filter == nil)\n        #expect(result.orderBy.isEmpty)\n    }\n\n    @Test\n    func custom() throws {\n        let data = \"\"\"\n            contentType: post\n            scope: list\n            limit: 1\n            offset: 0\n            filter:\n                key: name\n                operator: equals\n                value: hello\n            orderBy:\n                - key: name\n                - key: other\n                  direction: desc\n            \"\"\"\n\n        let decoder = ToucanYAMLDecoder()\n\n        let result = try decoder.decode(\n            Query.self,\n            from: data\n        )\n\n        #expect(result.contentType == \"post\")\n        #expect(result.scope == Pipeline.Scope.Keys.list.rawValue)\n        #expect(result.limit == 1)\n        #expect(result.offset == 0)\n\n        guard case let .field(key, op, value) = result.filter else {\n            Issue.record(\"Result is not a field case.\")\n            return\n        }\n\n        #expect(key == \"name\")\n        #expect(op == .equals)\n        #expect(value.value(as: String.self) == \"hello\")\n\n        try #require(result.orderBy.count == 2)\n        #expect(result.orderBy[0].key == \"name\")\n        #expect(result.orderBy[0].direction == .asc)\n        #expect(result.orderBy[1].key == \"other\")\n        #expect(result.orderBy[1].direction == .desc)\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSourceTests/Objects/Relation/RelationTestSuite.swift",
    "content": "//\n//  RelationTestSuite.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 03. 12..\n\nimport Foundation\nimport Testing\nimport ToucanSerialization\n\n@testable import ToucanSource\n\n@Suite\nstruct RelationTestSuite {\n\n    @Test\n    func basicOrdering() throws {\n        let data = \"\"\"\n            references: post\n            type: many\n            order: \n                key: title\n                direction: desc\n            \"\"\"\n            .data(using: .utf8)!\n\n        let decoder = ToucanYAMLDecoder()\n\n        let result = try decoder.decode(\n            Relation.self,\n            from: data\n        )\n\n        #expect(result.references == \"post\")\n        #expect(result.type == .many)\n        #expect(result.order?.key == \"title\")\n        #expect(result.order?.direction == .desc)\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSourceTests/Objects/Relation/RelationTypeTestSuite.swift",
    "content": "//\n//  RelationTypeTestSuite.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 05. 18..\n\nimport Foundation\nimport Testing\nimport ToucanSerialization\n\n@testable import ToucanSource\n\n@Suite\nstruct RelationTypeTestSuite {\n    @Test\n    func basics() throws {\n        let object = [RelationType.one, RelationType.many]\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n\n        let value1: String = try encoder.encode(object)\n        let result1 = try decoder.decode([RelationType].self, from: value1)\n\n        let value2: Data = try encoder.encode(object)\n        let result2 = try decoder.decode([RelationType].self, from: value2)\n\n        #expect(object == result1)\n        #expect(object == result2)\n    }\n\n    @Test\n    func one() throws {\n        let value = \"one\"\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(RelationType.self, from: value)\n        let expectation = RelationType.one\n\n        #expect(result == expectation)\n    }\n\n    @Test\n    func many() throws {\n        let value = \"many\"\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(RelationType.self, from: value)\n        let expectation = RelationType.many\n\n        #expect(result == expectation)\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSourceTests/Objects/Settings/SettingsTestSuite.swift",
    "content": "//\n//  SettingsTestSuite.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 03. 30..\n\nimport Foundation\nimport Testing\nimport ToucanSerialization\n\n@testable import ToucanSource\n\n@Suite\nstruct SettingsTestSuite {\n\n    @Test\n    func defaults() throws {\n        let object = Settings.defaults\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n\n        let value1: String = try encoder.encode(object)\n        let result1 = try decoder.decode(Settings.self, from: value1)\n\n        let value2: Data = try encoder.encode(object)\n        let result2 = try decoder.decode(Settings.self, from: value2)\n\n        #expect(object == result1)\n        #expect(object == result2)\n    }\n\n    @Test\n    func empty() throws {\n        let value = \"\"\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(Settings.self, from: value)\n        let expectation = Settings.defaults\n\n        #expect(result == expectation)\n    }\n\n    @Test\n    func custom() throws {\n        let value = \"\"\"\n            foo: bar\n            \"\"\" + \"\\n\"\n\n        let encoder = ToucanYAMLEncoder()\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(Settings.self, from: value)\n\n        var expectation = Settings.defaults\n        expectation.values[\"foo\"] = \"bar\"\n\n        let encodedValue: String = try encoder.encode(expectation)\n\n        #expect(result == expectation)\n        #expect(value == encodedValue)\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSourceTests/Objects/Target/TargetConfigTestSuite.swift",
    "content": "//\n//  TargetConfigTestSuite.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 15..\n//\n\nimport Foundation\nimport Testing\nimport ToucanSerialization\n\n@testable import ToucanSource\n\n@Suite\nstruct TargetConfigTestSuite {\n    @Test\n    func full() throws {\n        let data = \"\"\"\n            targets:\n              - name: dev\n                config: \"./dev.yml\"\n                url: \"http://localhost:3000\"\n                output: \"./dist/\"\n                default: true\n              - name: live\n                url: \"https://example.com\"\n            \"\"\"\n            .data(using: .utf8)!\n\n        let decoder = ToucanYAMLDecoder()\n\n        let result = try decoder.decode(\n            TargetConfig.self,\n            from: data\n        )\n\n        #expect(result.targets.count == 2)\n        #expect(result.default.name == \"dev\")\n        #expect(result.default.isDefault == true)\n        #expect(result.targets[1].name == \"live\")\n    }\n\n    @Test\n    func defaultFallbackToFirst() throws {\n        let data = \"\"\"\n            targets:\n              - name: fallback\n              - name: another\n            \"\"\"\n            .data(using: .utf8)!\n\n        let decoder = ToucanYAMLDecoder()\n\n        let result = try decoder.decode(\n            TargetConfig.self,\n            from: data\n        )\n\n        #expect(result.targets.count == 2)\n        #expect(result.default.name == \"fallback\")\n        #expect(result.default.isDefault == true)\n    }\n\n    @Test\n    func oneDefaultIsValid() throws {\n        let data = \"\"\"\n            targets:\n              - name: one\n                default: true\n              - name: two\n            \"\"\"\n            .data(using: .utf8)!\n\n        let decoder = ToucanYAMLDecoder()\n\n        let result = try decoder.decode(TargetConfig.self, from: data)\n\n        #expect(result.targets.count == 2)\n        #expect(result.default.name == \"one\")\n    }\n\n    @Test\n    func noDefaultFallsBackToFirst() throws {\n        let data = \"\"\"\n            targets:\n              - name: alpha\n              - name: beta\n            \"\"\"\n            .data(using: .utf8)!\n\n        let decoder = ToucanYAMLDecoder()\n\n        let result = try decoder.decode(TargetConfig.self, from: data)\n\n        #expect(result.targets.count == 2)\n        #expect(result.default.name == \"alpha\")\n        #expect(result.default.isDefault == true)\n    }\n\n    @Test\n    func multipleDefaultsThrows() throws {\n        let data = \"\"\"\n            targets:\n              - name: foo\n                default: true\n              - name: bar\n                default: true\n            \"\"\"\n            .data(using: .utf8)!\n\n        let decoder = ToucanYAMLDecoder()\n        #expect(throws: (any Error).self) {\n            _ = try decoder.decode(TargetConfig.self, from: data)\n        }\n    }\n\n    @Test\n    func emptyListFallsBackToDefaults() throws {\n        let data = \"\"\"\n            targets: []\n            \"\"\"\n            .data(using: .utf8)!\n\n        let decoder = ToucanYAMLDecoder()\n\n        let result = try decoder.decode(TargetConfig.self, from: data)\n\n        #expect(!result.targets.isEmpty)\n        #expect(result.default == Target.standard)\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSourceTests/Objects/Target/TargetTestSuite.swift",
    "content": "//\n//  TargetTestSuite.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 15..\n\nimport Foundation\nimport Testing\nimport ToucanSerialization\n\n@testable import ToucanSource\n\n@Suite\nstruct TargetTestSuite {\n    @Test\n    func full() throws {\n        let data = \"\"\"\n            name: \"dev\"\n            config: \"./some-config.yml\"\n            url: \"https://example.com\"\n            output: \"./out\"\n            default: true\n            \"\"\"\n            .data(using: .utf8)!\n\n        let decoder = ToucanYAMLDecoder()\n\n        let result = try decoder.decode(\n            Target.self,\n            from: data\n        )\n\n        #expect(result.name == \"dev\")\n        #expect(result.config == \"./some-config.yml\")\n        #expect(result.url == \"https://example.com\")\n        #expect(result.output == \"./out\")\n        #expect(result.isDefault == true)\n    }\n\n    @Test\n    func defaults() throws {\n        let data = \"\"\"\n            name: \"dev\"\n            \"\"\"\n            .data(using: .utf8)!\n\n        let decoder = ToucanYAMLDecoder()\n\n        let result = try decoder.decode(\n            Target.self,\n            from: data\n        )\n\n        #expect(result.name == \"dev\")\n        #expect(result.config == \"\")\n        #expect(result.url == \"http://localhost:3000\")\n        #expect(result.output == \"dist\")\n        #expect(result.isDefault == false)\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSourceTests/Objects/Types/TypesTestSuite.swift",
    "content": "//\n//  TypesTestSuite.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 03. 30..\n\nimport Foundation\nimport Testing\nimport ToucanSerialization\n\n@testable import ToucanSource\n\n@Suite\nstruct TypesTestSuite {\n\n    @Test\n    func minimal() throws {\n        let data = \"\"\"\n            id: post\n            \"\"\"\n\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(ContentType.self, from: data)\n\n        #expect(result.id == \"post\")\n    }\n\n    @Test\n    func complex() throws {\n        let data = \"\"\"\n            id: post\n            properties: \n                title: \n                    type: string\n                publication:\n                    type: date\n                    config: \n                        format: \"y.m.d\"\n                    required: true\n            relations:\n                authors:\n                    references: author\n                    type: many\n                    order: \n                        key: name\n                tags:\n                    references: tag\n                    type: many\n                    order: \n                        key: priority\n                        direction: desc\n            queries:\n                prev:\n                    contentType: post\n                    limit: 1\n                    filter:\n                        key: publication\n                        operator: lessThan\n                        value: \"{{publication}}\"\n                    orderBy:\n                        - key: publication\n                          direction: desc\n\n                next:\n                    contentType: post\n                    limit: 1\n                    filter:\n                        key: publication\n                        operator: greaterThan\n                        value: \"{{publication}}\"\n                    orderBy:\n                        - key: publication\n                          direction: asc\n\n                related:\n                    contentType: post\n                    limit: 4\n                    filter:\n                        and:\n                            - key: authors\n                              operator: matching\n                              value: \"{{authors}}\"\n\n                            - key: id\n                              operator: notEquals\n                              value: \"{{id}}\"\n\n                similar:\n                    contentType: post\n                    limit: 4\n                    filter:\n                        and:\n                            - key: tags\n                              operator: matching\n                              value: \"{{tags}}\"\n\n                            - key: id\n                              operator: notEquals\n                              value: \"{{id}}\"\n\n\n            \"\"\"\n\n        let decoder = ToucanYAMLDecoder()\n        let result = try decoder.decode(ContentType.self, from: data)\n\n        #expect(result.id == \"post\")\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSourceTests/RawContentLoaderTestSuite.swift",
    "content": "//\n//  RawContentLoaderTestSuite.swift\n//  Toucan\n//\n//  Created by Tibor Bödecs on 2025. 05. 19..\n//\n\nimport FileManagerKit\nimport FileManagerKitBuilder\nimport Foundation\nimport Logging\nimport Testing\nimport ToucanSerialization\n\n@testable import ToucanSource\n\nextension RawContent {\n\n    var withoutLastModificationDatePrecision: Self {\n        .init(\n            origin: origin,\n            markdown: markdown,\n            lastModificationDate: Double(Float(lastModificationDate)),\n            assetsPath: assetsPath,\n            assets: assets\n        )\n    }\n}\n\n@Suite\nstruct RawContentLoaderTestSuite {\n\n    private func testSourceContentsHierarchy(\n        @FileManagerPlayground.ItemBuilder _ builder: () ->\n            [FileManagerPlayground.Item]\n    ) -> Directory {\n        Directory(name: \"src\") {\n            Directory(name: \"contents\", builder)\n        }\n    }\n\n    private func testRawContentLoader(\n        fileManager: FileManagerKit,\n        url: URL\n    ) -> RawContentLoader {\n        let url = url.appending(path: \"./src/\")\n        let decoder = ToucanYAMLDecoder()\n        let config = Config.defaults\n        let locations = BuiltTargetSourceLocations(\n            sourceURL: url,\n            config: config\n        )\n        let loader = RawContentLoader(\n            contentsURL: locations.contentsURL,\n            assetsPath: config.contents.assets.path,\n            decoder: .init(),\n            markdownParser: .init(decoder: decoder),\n            fileManager: fileManager\n        )\n        return loader\n    }\n\n    // MARK: - locate origin index file types\n\n    private func testBlogArticleHierarchy(\n        @FileManagerPlayground.ItemBuilder _ builder: () ->\n            [FileManagerPlayground.Item]\n    ) -> Directory {\n        testSourceContentsHierarchy {\n            Directory(name: \"blog\") {\n                Directory(name: \"articles\") {\n                    \"noindex.yaml\"\n                    Directory(name: \"first-beta-release\", builder)\n                }\n            }\n        }\n    }\n\n    private func testBlogArticleOrigin() -> Origin {\n        .init(\n            path: .init(\"blog/articles/first-beta-release\"),\n            slug: \"blog/first-beta-release\"\n        )\n    }\n\n    private func testExpectationRequirements(\n        fileManager: FileManagerKit,\n        url: URL\n    ) throws {\n        let loader = testRawContentLoader(fileManager: fileManager, url: url)\n        let results = loader.locateOrigins()\n        #expect(results.count == 1)\n\n        let result = try #require(results.first)\n        let expected = testBlogArticleOrigin()\n\n        #expect(result == expected)\n    }\n\n    // MARK: - origins\n\n    @Test()\n    func locateOriginsEmptyResults() async throws {\n        try FileManagerPlayground()\n            .test {\n                let loader = testRawContentLoader(fileManager: $0, url: $1)\n                let results = loader.locateOrigins()\n                #expect(results.isEmpty)\n            }\n    }\n\n    @Test()\n    func locateOriginsWithNoIndexFile() async throws {\n        try FileManagerPlayground {\n            testSourceContentsHierarchy {\n                Directory(name: \"blog\") {\n                    Directory(name: \"articles\") {\n                        \"noindex.yaml\"\n                        Directory(name: \"first-beta-release\") {\n                            \"index.md\"\n                        }\n                    }\n                }\n            }\n        }\n        .test {\n            let loader = testRawContentLoader(fileManager: $0, url: $1)\n            let results = loader.locateOrigins()\n            #expect(results.count == 1)\n\n            let result = try #require(results.first)\n            let expected = Origin(\n                path: .init(\"blog/articles/first-beta-release\"),\n                slug: \"blog/first-beta-release\"\n            )\n            #expect(result == expected)\n        }\n    }\n\n    @Test()\n    func locateOriginsIgnoreSlugBrackets() async throws {\n        try FileManagerPlayground {\n            testSourceContentsHierarchy {\n                Directory(name: \"[01]blog\") {\n                    Directory(name: \"[01]articles\") {\n                        Directory(name: \"[01]first-beta-release\") {\n                            \"index.md\"\n                        }\n                    }\n                }\n            }\n        }\n        .test {\n            let loader = testRawContentLoader(fileManager: $0, url: $1)\n            let results = loader.locateOrigins()\n            #expect(results.count == 1)\n\n            let result = try #require(results.first)\n            let expected = Origin(\n                path: .init(\n                    \"[01]blog/[01]articles/[01]first-beta-release\"\n                ),\n                slug: \"blog/articles/first-beta-release\"\n            )\n            #expect(result == expected)\n        }\n    }\n\n    @Test()\n    func locateOriginsNoIndexBrackets() async throws {\n        try FileManagerPlayground {\n            testSourceContentsHierarchy {\n                Directory(name: \"[01]blog\") {\n                    Directory(name: \"[articles]\") {\n                        Directory(name: \"[02]first-beta-release\") {\n                            \"index.md\"\n                        }\n                    }\n                }\n            }\n        }\n        .test {\n            let loader = testRawContentLoader(fileManager: $0, url: $1)\n            let results = loader.locateOrigins()\n            #expect(results.count == 1)\n\n            let result = try #require(results.first)\n            let expected = Origin(\n                path: .init(\n                    \"[01]blog/[articles]/[02]first-beta-release\"\n                ),\n                slug: \"blog/first-beta-release\"\n            )\n            #expect(result == expected)\n        }\n    }\n\n    @Test(\n        arguments: [\n            [\"index.md\"],\n            [\"index.markdown\"],\n            [\"index.yml\"],\n            [\"index.yaml\"],\n            [\"index.yml\", \"index.yaml\"],\n            [\"index.md\", \"index.markdown\"],\n            [\"index.md\", \"index.markdown\", \"index.yml\", \"index.yaml\"],\n        ]\n    )\n    func locateFiles(files: [String]) async throws {\n        try FileManagerPlayground {\n            testBlogArticleHierarchy { files.map { .file(.init(name: $0)) } }\n        }\n        .test {\n            try testExpectationRequirements(fileManager: $0, url: $1)\n        }\n    }\n\n    // MARK: - loading contents\n\n    func testMarkdownFile(\n        ext: String,\n        modificationDate: Date\n    ) -> File {\n        File(\n            name: \"index.\\(ext)\",\n            attributes: [\n                .modificationDate: modificationDate\n            ],\n            string: \"\"\"\n                ---\n                title: \"Hello index.\\(ext)\"\n                ---\n\n                # Hello index.\\(ext)\n\n                Lorem ipsum dolor sit amet\n                \"\"\"\n        )\n    }\n\n    func testYAMLFile(\n        ext: String,\n        modificationDate: Date\n    ) -> File {\n        File(\n            name: \"index.\\(ext)\",\n            attributes: [\n                .modificationDate: modificationDate\n            ],\n            string: \"\"\"\n                title: \"Hello index.\\(ext)\"\n                \"\"\"\n        )\n    }\n\n    func testAssetsDirectory() -> Directory {\n        Directory(name: \"assets\") {\n            \"cover.png\"\n            \"main.js\"\n            \"style.css\"\n        }\n    }\n\n    func testExpectedRawContent(\n        ext: String,\n        emptyContents: Bool,\n        modificationDate: Date\n    ) -> RawContent {\n        .init(\n            origin: testBlogArticleOrigin(),\n            markdown: .init(\n                frontMatter: [\n                    \"title\": \"Hello index.\\(ext)\"\n                ],\n                contents: emptyContents\n                    ? \"\"\n                    : \"\"\"\n                    # Hello index.\\(ext)\n\n                    Lorem ipsum dolor sit amet\n                    \"\"\"\n            ),\n            lastModificationDate: modificationDate.timeIntervalSince1970,\n            assetsPath: \"assets\",\n            assets: [\n                \"cover.png\",\n                \"main.js\",\n                \"style.css\",\n            ]\n            .sorted()\n        )\n    }\n\n    @Test(\n        arguments: [\n            \"md\",\n            \"markdown\",\n        ]\n    )\n    func loadMarkdownContents(ext: String) async throws {\n        let now = Date()\n\n        try FileManagerPlayground {\n            testBlogArticleHierarchy {\n                testAssetsDirectory()\n                testMarkdownFile(ext: ext, modificationDate: now)\n            }\n        }\n        .test {\n            let loader = testRawContentLoader(fileManager: $0, url: $1)\n            let results = loader.locateOrigins()\n            #expect(results.count == 1)\n\n            let origin = try #require(results.first)\n            let content = try loader.loadRawContent(at: origin)\n\n            let expectation = testExpectedRawContent(\n                ext: ext,\n                emptyContents: false,\n                modificationDate: now\n            )\n\n            #expect(\n                content.withoutLastModificationDatePrecision\n                    == expectation.withoutLastModificationDatePrecision\n            )\n        }\n    }\n\n    @Test(\n        arguments: [\n            \"yml\",\n            \"yaml\",\n        ]\n    )\n    func loadYAMLContents(ext: String) async throws {\n        let now = Date()\n\n        try FileManagerPlayground {\n            testBlogArticleHierarchy {\n                testAssetsDirectory()\n                testYAMLFile(ext: ext, modificationDate: now)\n            }\n        }\n        .test {\n            let loader = testRawContentLoader(fileManager: $0, url: $1)\n            let results = loader.locateOrigins()\n            #expect(results.count == 1)\n\n            let origin = try #require(results.first)\n            let content = try loader.loadRawContent(at: origin)\n\n            let expectation = testExpectedRawContent(\n                ext: ext,\n                emptyContents: true,\n                modificationDate: now\n            )\n\n            #expect(\n                content.withoutLastModificationDatePrecision\n                    == expectation.withoutLastModificationDatePrecision\n            )\n        }\n    }\n\n    @Test()\n    func loadMergedFileContents() async throws {\n        let now = Date()\n\n        try FileManagerPlayground {\n            testBlogArticleHierarchy {\n                Directory(name: \"assets\") {\n                    \"cover.png\"\n                    \"style.css\"\n                    \"main.js\"\n                }\n                testMarkdownFile(ext: \"md\", modificationDate: now)\n                testMarkdownFile(ext: \"markdown\", modificationDate: now)\n                testYAMLFile(ext: \"yml\", modificationDate: now)\n                testYAMLFile(ext: \"yaml\", modificationDate: now)\n            }\n        }\n        .test {\n            let loader = testRawContentLoader(fileManager: $0, url: $1)\n            let results = loader.locateOrigins()\n            #expect(results.count == 1)\n\n            let origin = try #require(results.first)\n            let content = try loader.loadRawContent(at: origin)\n\n            let exp = RawContent(\n                origin: testBlogArticleOrigin(),\n                markdown: .init(\n                    frontMatter: [\n                        \"title\": \"Hello index.yml\"\n                    ],\n                    contents: \"\"\"\n                        # Hello index.md\n\n                        Lorem ipsum dolor sit amet\n                        \"\"\"\n                ),\n                lastModificationDate: now.timeIntervalSince1970,\n                assetsPath: \"assets\",\n                assets: [\n                    \"cover.png\",\n                    \"main.js\",\n                    \"style.css\",\n                ]\n                .sorted()\n            )\n\n            #expect(content.origin == exp.origin)\n            #expect(content.markdown == exp.markdown)\n            #expect(content.markdown.frontMatter == exp.markdown.frontMatter)\n            #expect(content.markdown.contents == exp.markdown.contents)\n            #expect(\n                Float(content.lastModificationDate)\n                    == Float(exp.lastModificationDate)\n            )\n            #expect(content.assets == exp.assets)\n            #expect(\n                content.withoutLastModificationDatePrecision\n                    == exp.withoutLastModificationDatePrecision\n            )\n        }\n    }\n\n    @Test()\n    func loadMultipleRawContents() async throws {\n        let now = Date()\n\n        try FileManagerPlayground {\n            testSourceContentsHierarchy {\n                Directory(name: \"example-1\") {\n                    testMarkdownFile(ext: \"md\", modificationDate: now)\n                }\n                Directory(name: \"example-2\") {\n                    testYAMLFile(ext: \"yml\", modificationDate: now)\n                }\n            }\n        }\n        .test {\n            let loader = testRawContentLoader(fileManager: $0, url: $1)\n            let results = try loader.load()\n            #expect(results.count == 2)\n\n            let expected: [RawContent] = [\n                .init(\n                    origin: .init(\n                        path: .init(\"example-1\"),\n                        slug: \"example-1\"\n                    ),\n                    markdown: .init(\n                        frontMatter: [\"title\": \"Hello index.md\"],\n                        contents: \"\"\"\n                            # Hello index.md\n\n                            Lorem ipsum dolor sit amet\n                            \"\"\"\n                    ),\n                    lastModificationDate: now.timeIntervalSince1970,\n                    assetsPath: \"assets\",\n                    assets: []\n                ),\n                .init(\n                    origin: .init(\n                        path: .init(\"example-2\"),\n                        slug: \"example-2\"\n                    ),\n                    markdown: .init(\n                        frontMatter: [\"title\": \"Hello index.yml\"],\n                        contents: \"\"\n                    ),\n                    lastModificationDate: now.timeIntervalSince1970,\n                    assetsPath: \"assets\",\n                    assets: []\n                ),\n            ]\n\n            #expect(\n                results.map(\\.withoutLastModificationDatePrecision)\n                    == expected.map(\\.withoutLastModificationDatePrecision)\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/ToucanSourceTests/TemplateLoaderTestSuite.swift",
    "content": "//\n//  TemplateLoaderTestSuite.swift\n//  Toucan\n//\n//  Created by Binary Birds on 2025. 03. 12..\n\nimport FileManagerKit\nimport FileManagerKitBuilder\nimport Foundation\nimport Logging\nimport Testing\nimport ToucanSerialization\n@testable import ToucanCore\n@testable import ToucanSource\n\n@Suite\nstruct TemplateLoaderTestSuite {\n\n    @Test()\n    func standardTemplateLoading() async throws {\n        try FileManagerPlayground {\n            Directory(name: \"src\") {\n                Directory(name: \"assets\") {\n                    \"style.css\"\n                }\n                Directory(name: \"contents\") {\n                    Directory(name: \"about\") {\n                        File(\n                            name: \"pages.about.mustache\",\n                            string: \"\"\"\n                                about content override\n                                \"\"\"\n                        )\n                    }\n                }\n                Directory(name: \"templates\") {\n                    Directory(name: \"default\") {\n                        File(\n                            name: \"template.yaml\",\n                            string: \"\"\"\n                                author:\n                                    name: Test Template Author\n                                    url: http://localhost:8080/\n                                demo:\n                                    url: http://localhost:8080/\n                                description: Test Template description\n                                generatorVersion:\n                                    value: \"1.0.0-beta.6\"\n                                    type: \"upNextMajor\"\n                                license:\n                                    name: Test License\n                                    url: http://localhost:8080/\n                                name: Test Template\n                                tags:\n                                    - blog\n                                    - adaptive-colors\n                                url: http://localhost:8080/\n                                version: 1.0.0\n                                \"\"\"\n                        )\n                        Directory(name: \"assets\") {\n                            \"template.css\"\n                        }\n                        Directory(name: \"views\") {\n                            Directory(name: \"pages\") {\n                                File(\n                                    name: \"default.mustache\",\n                                    string: \"\"\"\n                                        default\n                                        \"\"\"\n                                )\n                                File(\n                                    name: \"about.mustache\",\n                                    string: \"\"\"\n                                        about\n                                        \"\"\"\n                                )\n                                File(\n                                    name: \"test.html\",\n                                    string: \"\"\"\n                                        test.html\n                                        \"\"\"\n                                )\n                            }\n                            File(\n                                name: \"html.mustache\",\n                                string: \"\"\"\n                                    html\n                                    \"\"\"\n                            )\n                            \"README.md\"\n                        }\n                        \"README.md\"\n                    }\n                    Directory(name: \"overrides\") {\n                        Directory(name: \"default\") {\n                            Directory(name: \"assets\") {\n                                \"template.css\"\n                            }\n                            Directory(name: \"views\") {\n                                Directory(name: \"pages\") {\n                                    File(\n                                        name: \"default.mustache\",\n                                        string: \"\"\"\n                                            default override\n                                            \"\"\"\n                                    )\n                                    File(\n                                        name: \"about.mustache\",\n                                        string: \"\"\"\n                                            about override\n                                            \"\"\"\n                                    )\n                                }\n                                \"README.md\"\n                            }\n                            \"README.md\"\n                        }\n                    }\n                }\n            }\n        }\n        .test {\n            let sourceURL = $1.appending(path: \"src/\")\n            let config = Config.defaults\n            let locations = BuiltTargetSourceLocations(\n                sourceURL: sourceURL,\n                config: config\n            )\n\n            let loader = TemplateLoader(\n                locations: locations,\n                fileManager: $0,\n                encoder: ToucanYAMLEncoder(),\n                decoder: ToucanYAMLDecoder()\n            )\n            let template = try loader.load()\n\n            #expect(\n                template.metadata.generatorVersion.value.description\n                    == \"1.0.0-beta.6\"\n            )\n            #expect(template.metadata.generatorVersion.type == .upNextMajor)\n\n            #expect(\n                template.components.assets.sorted()\n                    == [\n                        \"template.css\"\n                    ]\n                    .sorted()\n            )\n            #expect(\n                template.components.views.map(\\.path).sorted()\n                    == [\n                        \"pages/default.mustache\",\n                        \"pages/about.mustache\",\n                        \"pages/test.html\",\n                        \"html.mustache\",\n                    ]\n                    .sorted()\n            )\n\n            #expect(\n                template.overrides.assets.sorted()\n                    == [\n                        \"template.css\"\n                    ]\n                    .sorted()\n            )\n            #expect(\n                template.overrides.views.map(\\.path).sorted()\n                    == [\n                        \"pages/about.mustache\",\n                        \"pages/default.mustache\",\n                    ]\n                    .sorted()\n            )\n\n            #expect(\n                template.content.assets.sorted()\n                    == [\n                        \"style.css\"\n                    ]\n                    .sorted()\n            )\n            #expect(\n                template.content.views.map(\\.path).sorted()\n                    == [\n                        \"about/pages.about.mustache\"\n                    ]\n                    .sorted()\n            )\n\n            let results = template.getViewIDsWithContents()\n\n            let exp: [String: String] = [\n                \"pages.test\": \"test.html\",\n                \"pages.about\": \"about content override\",\n                \"pages.default\": \"default override\",\n                \"html\": \"html\",\n            ]\n\n            #expect(\n                results\n                    == .init(\n                        uniqueKeysWithValues: exp.sorted { $0.key < $1.key }\n                    )\n            )\n        }\n    }\n\n    @Test\n    func defaultGeneratorVersionComparisonType() async throws {\n        try FileManagerPlayground {\n            Directory(name: \"src\") {\n                Directory(name: \"templates\") {\n                    Directory(name: \"default\") {\n                        File(\n                            name: \"template.yaml\",\n                            string: \"\"\"\n                                author:\n                                    name: Test Template Author\n                                    url: http://localhost:8080/\n                                demo:\n                                    url: http://localhost:8080/\n                                description: Test Template description\n                                generatorVersion:\n                                    value: \"1.0.0-beta.6\"\n                                license:\n                                    name: Test License\n                                    url: http://localhost:8080/\n                                name: Test Template\n                                tags:\n                                    - blog\n                                    - adaptive-colors\n                                url: http://localhost:8080/\n                                version: 1.0.0\n                                \"\"\"\n                        )\n                    }\n                }\n            }\n        }\n        .test {\n            let sourceURL = $1.appending(path: \"src/\")\n            let config = Config.defaults\n            let locations = BuiltTargetSourceLocations(\n                sourceURL: sourceURL,\n                config: config\n            )\n\n            let loader = TemplateLoader(\n                locations: locations,\n                fileManager: $0,\n                encoder: ToucanYAMLEncoder(),\n                decoder: ToucanYAMLDecoder()\n            )\n            let template = try loader.load()\n\n            #expect(\n                template.metadata.generatorVersion.value.description\n                    == \"1.0.0-beta.6\"\n            )\n            #expect(template.metadata.generatorVersion.type == .upNextMajor)\n        }\n    }\n\n    @Test()\n    func invalidGeneratorVersion() async throws {\n        try FileManagerPlayground {\n            Directory(name: \"src\") {\n                Directory(name: \"templates\") {\n                    Directory(name: \"default\") {\n                        File(\n                            name: \"template.yaml\",\n                            string: \"\"\"\n                                author:\n                                    name: Test Template Author\n                                    url: http://localhost:8080/\n                                demo:\n                                    url: http://localhost:8080/\n                                description: Test Template description\n                                generatorVersion:\n                                    value: \"invalid\"\n                                    type: \"upNextMajor\"\n                                license:\n                                    name: Test License\n                                    url: http://localhost:8080/\n                                name: Test Template\n                                tags:\n                                    - blog\n                                    - adaptive-colors\n                                url: http://localhost:8080/\n                                version: 1.0.0\n                                \"\"\"\n                        )\n                    }\n                }\n            }\n        }\n        .test {\n            let sourceURL = $1.appending(path: \"src/\")\n            let config = Config.defaults\n            let locations = BuiltTargetSourceLocations(\n                sourceURL: sourceURL,\n                config: config\n            )\n\n            let loader = TemplateLoader(\n                locations: locations,\n                fileManager: $0,\n                encoder: ToucanYAMLEncoder(),\n                decoder: ToucanYAMLDecoder()\n            )\n\n            do {\n                let _ = try loader.load()\n                Issue.record(\"Expected DecodingError.dataCorrupted.\")\n            }\n            catch let error as ToucanError {\n                guard\n                    let objectLoaderError = error.lookup(\n                        ObjectLoaderError.self\n                    ),\n                    let toucanDecoderError = objectLoaderError.lookup(\n                        ToucanDecoderError.self\n                    ),\n                    let decodingError = toucanDecoderError.lookup(\n                        DecodingError.self\n                    ),\n                    case let DecodingError.dataCorrupted(context) =\n                        decodingError\n                else {\n                    throw error\n                }\n\n                let expected = \"Invalid semantic version\"\n                #expect(context.debugDescription == expected)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "scripts/install-toucan.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nlog() { printf -- \"** %s\\n\" \"$*\" >&2; }\nerror() { printf -- \"** ERROR: %s\\n\" \"$*\" >&2; }\nfatal() { error \"$@\"; exit 1; }\n\nswift build -c release\n\nINSTALL_DIR=\"${1:-/usr/local/bin}\"\n\nif [ ! -d \"$INSTALL_DIR\" ]; then\n    sudo mkdir -p \"$INSTALL_DIR\"\nfi\n\nfor binary in toucan toucan-generate toucan-init toucan-serve toucan-watch; do\n  sudo install .build/release/${binary} \"${INSTALL_DIR}/${binary}\"\ndone\n"
  },
  {
    "path": "scripts/packaging/deb.sh",
    "content": "#!/bin/bash\nset -e\n\nVERSION=\"$1\"\nNAME=\"toucan\"\n\nif [ -z \"$VERSION\" ]; then\n  echo \"Usage: $0 <VERSION>\"\n  exit 1\nfi\n\nARCH=\"amd64\"\nBUILD_DIR=\"build-deb\"\nPKG_ROOT=\"$BUILD_DIR/${NAME}_${VERSION}\"\nINSTALL_PREFIX=\"/usr/local/bin\"\nBIN_DIR=\".build/release\"\nBINARY_NAMES=(\"toucan\" \"toucan-generate\" \"toucan-init\" \"toucan-serve\" \"toucan-watch\")\n\necho \"📦 Building .deb for $NAME version $VERSION\"\n\n# Collect matching executables\nEXECUTABLES=\"\"\nfor BINNAME in \"${BINARY_NAMES[@]}\"; do\n  CANDIDATE=\"$BIN_DIR/$BINNAME\"\n  if [ -f \"$CANDIDATE\" ] && [ -x \"$CANDIDATE\" ]; then\n    EXECUTABLES+=\"$CANDIDATE\"$'\\n'\n  else\n    echo \"⚠️ Skipping missing or non-executable: $BINNAME\"\n  fi\ndone\n\nif [ -z \"$EXECUTABLES\" ]; then\n  echo \"❌ No executable binaries found\"\n  exit 1\nfi\n\n# Prepare package directory structure\nrm -rf \"$PKG_ROOT\"\nmkdir -p \"$PKG_ROOT/DEBIAN\"\nmkdir -p \"$PKG_ROOT$INSTALL_PREFIX\"\n\n# Copy binaries\nwhile IFS= read -r BIN; do\n  [ -z \"$BIN\" ] && continue\n  BASENAME=$(basename \"$BIN\")\n  cp \"$BIN\" \"$PKG_ROOT$INSTALL_PREFIX/$BASENAME\"\n  chmod +x \"$PKG_ROOT$INSTALL_PREFIX/$BASENAME\"\n  echo \"✅ Added $BASENAME\"\ndone <<< \"$EXECUTABLES\"\n\ncat > \"$PKG_ROOT/DEBIAN/control\" <<EOF\nPackage: $NAME\nVersion: $VERSION\nArchitecture: $ARCH\nMaintainer: binarybirds <info@binarybirds.com>\nDescription: $NAME is a static site generator written in Swift.\nSection: utils\nPriority: optional\nEOF\n\ndpkg-deb --build \"$PKG_ROOT\"\nCUSTOM_NAME=\"toucan-linux-amd64-${VERSION}.deb\"\nmv \"$PKG_ROOT.deb\" \"$BUILD_DIR/$CUSTOM_NAME\"\necho \"🎉 DEB created: $BUILD_DIR/$CUSTOM_NAME\""
  },
  {
    "path": "scripts/packaging/dmg.sh",
    "content": "#!/bin/bash\nset -e\n\nVERSION=\"$1\"\nDMG_NAME=\"toucan-macos-${VERSION}.dmg\"\nVOL_NAME=\"Toucan Installer\"\nPKG_NAME=\"toucan-macos-${VERSION}.pkg\"\n\nRELEASE_DIR=\"$(pwd)/release\"\nPKG_PATH=\"${RELEASE_DIR}/${PKG_NAME}\"\nDMG_PATH=\"${RELEASE_DIR}/${DMG_NAME}\"\nDMG_ROOT=\"$(pwd)/dmg-root\"\nLICENSE_SOURCE=\"./LICENSE\"\n\nif [ -z \"$VERSION\" ]; then\n  echo \"Usage: $0 <VERSION>\"\n  exit 1\nfi\n\nif [ ! -f \"$PKG_PATH\" ]; then\n  echo \"❌ .pkg file not found at $PKG_PATH\"\n  exit 1\nfi\n\necho \"📦 Creating .dmg from $PKG_NAME\"\n\nrm -rf \"$DMG_ROOT\"\nmkdir -p \"$DMG_ROOT\"\n\n# Copy and rename .pkg\ncp \"$PKG_PATH\" \"$DMG_ROOT/Toucan.pkg\"\n\n# Copy LICENSE file\nif [ -f \"$LICENSE_SOURCE\" ]; then\n  cp \"$LICENSE_SOURCE\" \"$DMG_ROOT/LICENSE\"\nelse\n  echo \"⚠️ LICENSE file not found at $LICENSE_SOURCE\"\nfi\n\n# Create README\ncat > \"$DMG_ROOT/README.txt\" <<EOF\nToucan Installer\n\nTo install:\n1. Double-click the 'Install Toucan' script\n2. Enter your admin password if prompted\n\nOr, install manually by double-clicking the 'Toucan.pkg' file.\nEOF\n\n# Create install script\ncat > \"$DMG_ROOT/Install Toucan.command\" <<EOF\n#!/bin/bash\nset -e\necho \"Launching Toucan Installer...\"\nopen \"/Volumes/${VOL_NAME}/Toucan.pkg\"\nEOF\n\nchmod +x \"$DMG_ROOT/Install Toucan.command\"\n\n# Optional: Sign the .command script\nif [[ -n \"$MAC_APP_IDENTITY\" ]]; then\n  echo \"🔏 Signing 'Install Toucan.command' with identity: $MAC_APP_IDENTITY\"\n  codesign --sign \"$MAC_APP_IDENTITY\" --force --timestamp --verbose \"$DMG_ROOT/Install Toucan.command\"\n  echo \"✅ Signed Install Toucan.command\"\nelse\n  echo \"⚠️ No codesign identity provided. Script remains unsigned.\"\nfi\n\n# Create the DMG\nhdiutil create \\\n  -volname \"$VOL_NAME\" \\\n  -srcfolder \"$DMG_ROOT\" \\\n  -ov \\\n  -format UDZO \\\n  \"$DMG_PATH\"\n\necho \"✅ .dmg created: $DMG_PATH\"\n\n# Optional: Sign the .dmg file\nif [[ -n \"$MAC_APP_IDENTITY\" ]]; then\n  echo \"🔏 Signing DMG with identity: MAC_APP_IDENTITY\"\n  codesign --sign \"$MAC_APP_IDENTITY\" --force --timestamp --verbose \"$DMG_PATH\"\n  echo \"✅ DMG signed: $DMG_PATH\"\nelse\n  echo \"⚠️ No codesign identity provided. DMG file remains unsigned.\"\nfi\n\n# Optional: Notarize the .dmg file with Apple\nif [[ -n \"$APPLE_ID\" && -n \"$APPLE_TEAM_ID\" && -n \"$APP_SPECIFIC_PASSWORD\" ]]; then\n  echo \"📤 Submitting $DMG_PATH for notarization...\"\n  xcrun notarytool submit \"$DMG_PATH\" \\\n    --apple-id \"$APPLE_ID\" \\\n    --team-id \"$APPLE_TEAM_ID\" \\\n    --password \"$APP_SPECIFIC_PASSWORD\" \\\n    --wait\n\n  echo \"📎 Stapling notarization ticket to $DMG_PATH\"\n  xcrun stapler staple \"$DMG_PATH\"\n  echo \"✅ Notarization and stapling complete\"\nelse\n  echo \"⚠️ Notarization skipped — missing APPLE_ID, TEAM_ID, or APP_PASSWORD\"\nfi"
  },
  {
    "path": "scripts/packaging/pkg.sh",
    "content": "#!/bin/bash\nset -e\n\nVERSION=\"$1\"\nNAME=\"toucan\"\n\nif [ -z \"$VERSION\" ]; then\n  echo \"Usage: $0 <VERSION>\"\n  exit 1\nfi\n\necho \"📦 Creating .pkg and .zip for $NAME version $VERSION\"\n\n# Paths\nROOT_DIR=$(pwd)\nUNIVERSAL_DIR=\"$ROOT_DIR/.build/universal\"\nRELEASE_DIR=\"$ROOT_DIR/release\"\nPKGROOT=\"$ROOT_DIR/pkg-root\"\nPKGFILE=\"$RELEASE_DIR/${NAME}-macos-${VERSION}.pkg\"\nZIPFILE=\"$RELEASE_DIR/${NAME}-macos-${VERSION}.zip\"\nSHAFILE=\"$RELEASE_DIR/${NAME}-macos-${VERSION}.sha256\"\nBINARIES=(\"toucan\" \"toucan-generate\" \"toucan-init\" \"toucan-serve\" \"toucan-watch\")\n\n# Cleanup\nrm -rf \"$UNIVERSAL_DIR\" \"$PKGROOT\"\nmkdir -p \"$UNIVERSAL_DIR\" \"$PKGROOT/usr/local/bin\" \"$RELEASE_DIR\"\n\n# Build universal binaries\nfor BIN in \"${BINARIES[@]}\"; do\n  ARM64_BIN=\".build/arm64-apple-macosx/release/$BIN\"\n  X86_64_BIN=\".build/x86_64-apple-macosx/release/$BIN\"\n  OUT=\"$UNIVERSAL_DIR/$BIN\"\n\n  if [[ -x \"$ARM64_BIN\" && -x \"$X86_64_BIN\" ]]; then\n    lipo -create \"$ARM64_BIN\" \"$X86_64_BIN\" -output \"$OUT\"\n    echo \"✅ Universal binary created: $BIN\"\n\n    if [[ -n \"$MAC_APP_IDENTITY\" ]]; then\n      codesign --sign \"$MAC_APP_IDENTITY\" \\\n               --options runtime \\\n               --timestamp \\\n               --verbose \\\n               --force \"$OUT\"\n      echo \"🔏 Signed: $BIN\"\n    fi\n\n    cp \"$OUT\" \"$PKGROOT/usr/local/bin/\"\n  else\n    echo \"⚠️ Skipping $BIN — missing architecture binary\"\n  fi\ndone\n\n# Add LICENSE\nmkdir -p \"$PKGROOT/usr/local/share/$NAME\"\ncp LICENSE \"$PKGROOT/usr/local/share/$NAME/LICENSE\" || echo \"⚠️ LICENSE not found\"\n\n# Create .pkg\npkgbuild \\\n  --identifier \"com.binarybirds.${NAME}\" \\\n  --version \"$VERSION\" \\\n  --install-location / \\\n  --root \"$PKGROOT\" \\\n  \"$PKGFILE\"\n\n# Sign .pkg\nif [[ -n \"$MAC_INSTALLER_IDENTITY\" ]]; then\n  TMPFILE=\"${PKGFILE}.tmp\"\n  productsign --sign \"$MAC_INSTALLER_IDENTITY\" \"$PKGFILE\" \"$TMPFILE\"\n  mv \"$TMPFILE\" \"$PKGFILE\"\n  echo \"✅ Signed .pkg: $PKGFILE\"\nelse\n  echo \"⚠️ No MAC_INSTALLER_IDENTITY provided\"\nfi\n\n# Notarize .pkg\nif [[ -n \"$APPLE_ID\" && -n \"$APPLE_TEAM_ID\" && -n \"$APP_SPECIFIC_PASSWORD\" ]]; then\n  echo \"📤 Notarizing .pkg\"\n  xcrun notarytool submit \"$PKGFILE\" \\\n    --apple-id \"$APPLE_ID\" \\\n    --team-id \"$APPLE_TEAM_ID\" \\\n    --password \"$APP_SPECIFIC_PASSWORD\" \\\n    --wait\n  xcrun stapler staple \"$PKGFILE\"\n  echo \"✅ Notarization complete\"\nelse\n  echo \"⚠️ Notarization skipped\"\nfi\n\n# Zip universal binaries\ncd \"$UNIVERSAL_DIR\"\nzip -r \"$ZIPFILE\" ./*\ncd \"$ROOT_DIR\"\n\n# SHA256 hash\nshasum -a 256 \"$ZIPFILE\" > \"$SHAFILE\"\necho \"✅ SHA256 written: $SHAFILE\""
  },
  {
    "path": "scripts/packaging/rpm.sh",
    "content": "#!/bin/bash\nset -e\n\nVERSION=\"$1\"\nNAME=\"toucan\"\n\nif [ -z \"$VERSION\" ]; then\n  echo \"Usage: $0 <VERSION>\"\n  exit 1\nfi\n\nTARBALL=\"${NAME}-${VERSION}.tar.gz\"\nTOPDIR=\"$HOME/rpmbuild\"\nBIN_DIR=\".build/release\"\nBUILD_DIR=\"build-rpm\"\nBINARY_NAMES=(\"toucan\" \"toucan-generate\" \"toucan-init\" \"toucan-serve\" \"toucan-watch\")\n\necho \"📦 Building RPM for $NAME version $VERSION\"\n\n# Prepare RPM directories\nmkdir -p \"$TOPDIR\"/{BUILD,RPMS,SOURCES,SPECS,SRPMS}\nmkdir -p \"$BUILD_DIR\"\nWORKDIR=$(mktemp -d)\ntrap 'rm -rf \"$WORKDIR\"' EXIT\n\n# Stage binaries\nSRC_DIR=\"$WORKDIR/${NAME}-${VERSION}/usr/local/bin\"\nmkdir -p \"$SRC_DIR\"\nEXECUTABLES=()\n\nfor BIN in \"${BINARY_NAMES[@]}\"; do\n  SRC=\"$BIN_DIR/$BIN\"\n  if [ -x \"$SRC\" ]; then\n    cp \"$SRC\" \"$SRC_DIR/\"\n    chmod +x \"$SRC_DIR/$BIN\"\n    EXECUTABLES+=(\"$BIN\")\n    echo \"✅ Staged: $BIN\"\n  else\n    echo \"⚠️ Skipped: $BIN\"\n  fi\ndone\n\nif [ ${#EXECUTABLES[@]} -eq 0 ]; then\n  echo \"❌ No valid executables found\"\n  exit 1\nfi\n\n# Optionally include license file and readme file\ncp -f LICENSE README.md \"$WORKDIR/${NAME}-${VERSION}/\" 2>/dev/null || echo \"ℹ️ File(s) not found\"\n\n# Create source tarball for rpmbuild\ntar -czf \"$TOPDIR/SOURCES/$TARBALL\" -C \"$WORKDIR\" \"${NAME}-${VERSION}\"\n\n# Copy .spec file\ncp \"./scripts/packaging/${NAME}.spec\" \"$TOPDIR/SPECS/\"\n\n# Build the RPM\nrpmbuild -ba \"$TOPDIR/SPECS/${NAME}.spec\" --define \"ver $VERSION\"\n\n# Copy and rename RPM\nFINAL_RPM=$(find \"$TOPDIR/RPMS\" -type f -name \"*.rpm\" | head -n1)\nRPM_OUTPUT=\"$BUILD_DIR/${NAME}-linux-x86_64-${VERSION}.rpm\"\ncp \"$FINAL_RPM\" \"$RPM_OUTPUT\"\necho \"🎉 RPM created: $RPM_OUTPUT\"\n\n# Create ZIP of raw binaries\nZIP_NAME=\"${NAME}-linux-${VERSION}.zip\"\nSHA_NAME=\"${NAME}-linux-${VERSION}.sha256\"\nZIP_DIR=\"$BUILD_DIR/bin\"\n\nrm -rf \"$ZIP_DIR\"\nmkdir -p \"$ZIP_DIR\"\n\nfor BIN in \"${EXECUTABLES[@]}\"; do\n  cp \"$BIN_DIR/$BIN\" \"$ZIP_DIR/\"\ndone\n\ncd \"$ZIP_DIR\"\nzip \"../$ZIP_NAME\" ./*\ncd - >/dev/null\n\n# Create SHA256\ncd \"$BUILD_DIR\"\nshasum -a 256 \"$ZIP_NAME\" > \"$SHA_NAME\"\ncd - >/dev/null\n\necho \"✅ ZIP created: $BUILD_DIR/$ZIP_NAME\"\necho \"✅ SHA256 created: $BUILD_DIR/$SHA_NAME\"\n"
  },
  {
    "path": "scripts/packaging/toucan.spec",
    "content": "Name:           toucan\nVersion:        %{ver}\nRelease:        1\nSummary:        A static site generator (SSG) written in Swift\n\nLicense:        MIT\nURL:            https://github.com/toucansites/toucan\nSource0:        %{name}-%{version}.tar.gz\n\nBuildArch:      x86_64\n\n%description\nToucan is a static site generator written in Swift.\n\n%prep\n%setup -q\n\n%build\necho \"Skipping build; using precompiled binaries.\"\n\n%install\nmkdir -p %{buildroot}/usr/local/bin\ncp -a usr/local/bin/* %{buildroot}/usr/local/bin/\n\n%files\n/usr/local/bin/*\n%dir /usr/local/bin\n\n%license LICENSE\n%doc README.md"
  },
  {
    "path": "scripts/run-chmod.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nlog() { printf -- \"** %s\\n\" \"$*\" >&2; }\nerror() { printf -- \"** ERROR: %s\\n\" \"$*\" >&2; }\nfatal() { error \"$@\"; exit 1; }\n\nCURRENT_SCRIPT_DIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && pwd )\"\nREPO_ROOT=\"$(git -C \"${CURRENT_SCRIPT_DIR}\" rev-parse --show-toplevel)\"\nchmod -R oug+x \"${REPO_ROOT}/scripts/\""
  },
  {
    "path": "scripts/uninstall-toucan.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\nlog() { printf -- \"** %s\\n\" \"$*\" >&2; }\nerror() { printf -- \"** ERROR: %s\\n\" \"$*\" >&2; }\nfatal() { error \"$@\"; exit 1; }\n\nINSTALL_DIR=\"${1:-/usr/local/bin}\"\n\nfor binary in toucan toucan-generate toucan-init toucan-serve toucan-watch; do\n  sudo rm -f \"${INSTALL_DIR}/${binary}\"\ndone\n"
  }
]