[
  {
    "path": ".editorconfig",
    "content": "# EditorConfig helps developers define and maintain consistent\n# coding styles between different editors and IDEs\n# editorconfig.org\n\nroot = true\n\n[*]\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\n\n[*.{md,txt}]\nindent_size = 4\ntrim_trailing_whitespace = false\n\n[*.{java,xml,xslt}]\nindent_size = 4"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "*       @ebullient\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yaml",
    "content": "name: Bug Report\ndescription: File a bug report\ntitle: \"🐛 \"\nlabels: [\"type: bug\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        ## Before you start\n\n        - [Check for updates](https://github.com/ebullient/ttrpg-convert-cli/releases) and make sure you're running the latest version.\n        - Look at existing bug reports to see if your issue has already been reported.\n        - Is this actually a bug?\n            - [Get Help](https://github.com/ebullient/ttrpg-convert-cli/tree/main#where-to-find-help) if you aren't confident you're doing things right.\n            - If this is something that you wish the tool could do, start a discussion or create a feature request instead.\n\n        > [!TIP]\n        >\n        > - 🚜 [**Review the changelog**](https://github.com/ebullient/ttrpg-convert-cli/blob/main/CHANGELOG.md) for new capabilities (✨) and breaking changes (🔥💥).\n        > - 🔮 Check out [**Conventions**](https://github.com/ebullient/ttrpg-convert-cli/blob/main/README.md#conventions) and  [**Recommendations**](https://github.com/ebullient/ttrpg-convert-cli/blob/main/README.md#recommendations-for-using-the-cli).\n\n  - type: textarea\n    id: the-problem\n    attributes:\n      label: Describe the bug\n      description: |\n        A clear and concise summary of what the bug is.\n\n        Include an example and/or specific details about the resource in question.\n    validations:\n      required: true\n\n  - type: markdown\n    attributes:\n      value: |\n        Please provide a log file.\n\n        Run the command that exibits this bug as you usually would, but add the `--log` option.\n        [Attach the `ttrpg-convert.out.txt` file](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/attaching-files) that is created as a result.\n\n  - type: textarea\n    id: expected\n    attributes:\n      label: Expected Behavior\n      description: What did you expect to happen instead?\n    validations:\n      required: true\n\n  - type: input\n    id: cli-version\n    attributes:\n      label: TTRPG CLI Version\n      description: Which version are you using? (paste the output of [`--version`](https://github.com/ebullient/ttrpg-convert-cli/tree/7650c2785f05051fa64d95ec1f49d664ce4c2805#convert-5etools-json-data))\n      placeholder: 2.3.18\n    validations:\n      required: true\n\n  - type: checkboxes\n    id: operating-systems\n    attributes:\n      label: Which Operating Systems have you experienced this on?\n      description: You may select more than one.\n      options:\n        - label: Android\n        - label: iPhone/iPad\n        - label: Linux\n        - label: macOS\n        - label: Windows\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: 'It would be nice if... '\ntitle: \"✨ \"\nlabels: enhancement\nassignees: ''\n\n---\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/actions/data-cache/action.yml",
    "content": "name: 'Tools data cache'\ndescription: 'find latest release; construct cache key; fetch/build cache'\n\ninputs:\n  failIfMissing:\n    description: \"Should the job fail if missing\"\n    default: \"false\"\n\noutputs:\n  cache_key:\n    description: Cache retrieval key\n    value: ${{ steps.test_data.outputs.cache_key }}\n\nruns:\n  using: \"composite\"\n  steps:\n\n    - name: 5eTools release cache key\n      id: test_data\n      shell: bash\n      run: |\n        LATEST_5E_VERSION=$(curl -sLH 'Accept: application/json' https://api.github.com/repos/5etools-mirror-3/5etools-src/releases/latest | jq -r .tag_name)\n        LATEST_PF2E_VERSION=$(curl -sLH 'Accept: application/json' https://api.github.com/repos/Pf2eToolsOrg/Pf2eTools/releases/latest | jq -r .tag_name)\n\n        LATEST_VERSION=5e.${LATEST_5E_VERSION}-Pf2e.${LATEST_PF2E_VERSION}\n        PREFIX=\"TestData-\"\n        KEY=${PREFIX}${LATEST_VERSION}\n\n        echo \"🔹 Use 5eTools $LATEST_5E_VERSION and Pf2eTools $LATEST_PF2E_VERSION\"\n        echo \"cache_key_prefix=${PREFIX}\" >> $GITHUB_OUTPUT\n        echo \"cache_key=${KEY}\" >> $GITHUB_OUTPUT\n\n        echo \"version_5e=${LATEST_5E_VERSION}\" >> $GITHUB_OUTPUT\n        echo \"version_pf2e=${LATEST_PF2E_VERSION}\" >> $GITHUB_OUTPUT\n\n    - name: Check Cache Data\n      id: test_data_check\n      uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0\n      with:\n        path: sources\n        key: ${{ steps.test_data.outputs.cache_key }}\n        fail-on-cache-miss: ${{ inputs.failIfMissing == 'true' }}\n        enableCrossOsArchive: true\n\n    - name: Download Test Data\n      id: test-data-download\n      if: steps.test_data_check.outputs.cache-hit != 'true'\n      shell: bash\n      env:\n        LATEST_5E_VERSION: ${{ steps.test_data.outputs.version_5e }}\n        LATEST_PF2E_VERSION: ${{ steps.test_data.outputs.version_pf2e }}\n        GITHUB_TOKEN: ${{ github.token }}\n      run: |\n        echo \"🔹 Download 5e $LATEST_5E_VERSION\"\n\n        # sparse clone of src\n        mkdir -p sources/5etools-src\n        git clone --quiet --depth=1 -b ${LATEST_5E_VERSION} \\\n            -c advice.detachedHead=false \\\n            --no-checkout \\\n            https://github.com/5etools-mirror-3/5etools-src.git sources/5etools-src\n        pushd sources/5etools-src\n          git sparse-checkout init\n          git sparse-checkout set data\n          git checkout ${LATEST_5E_VERSION}\n          rm -rf .git\n          rm -f *.png\n          rm -f *.html\n          rm -f *.zip\n        popd\n\n        mkdir -p sources/5etools-img\n        git clone -b ${LATEST_5E_VERSION} \\\n            --single-branch --depth=1 \\\n            -c advice.detachedHead=false \\\n            https://github.com/5etools-mirror-3/5etools-img.git sources/5etools-img\n        pushd sources/5etools-img\n          du -sh .\n          # replace all images w/ empty files. We don't care about image content for test purposes\n          find . -type f -not -path '*/.git*' | while read FILE; do echo > \"$FILE\"; done\n          du -sh .\n        popd\n\n        echo \"🔹 5eTools: Download Homebrew\"\n\n        # Don't grab all of homebrew. Too big\n        mkdir -p sources/5e-homebrew/adventure\n        mkdir -p sources/5e-homebrew/background\n        mkdir -p sources/5e-homebrew/book\n        mkdir -p sources/5e-homebrew/class\n        mkdir -p sources/5e-homebrew/collection\n        mkdir -p sources/5e-homebrew/creature\n        mkdir -p sources/5e-homebrew/deity\n        mkdir -p sources/5e-homebrew/optionalfeature\n        mkdir -p sources/5e-homebrew/race\n        mkdir -p sources/5e-homebrew/spell\n        mkdir -p sources/5e-homebrew/subclass\n\n        paths=(\n        \"collection/MCDM Productions; Strongholds and Followers.json\"\n        \"adventure/Anthony Joyce; The Blood Hunter Adventure.json\"\n        \"adventure/Arcanum Worlds; Odyssey of the Dragonlords.json\"\n        \"adventure/JVC Parry; Call from the Deep.json\"\n        \"adventure/Kobold Press; Book of Lairs.json\"\n        \"background/D&D Wiki; Featured Quality Backgrounds.json\"\n        \"book/Ghostfire Gaming; Grim Hollow Campaign Guide.json\"\n        \"book/Ghostfire Gaming; Stibbles Codex of Companions.json\"\n        \"book/MCDM Productions; Arcadia Issue 3.json\"\n        \"class/D&D Wiki; Swashbuckler.json\"\n        \"class/Foxfire94; Vampire.json\"\n        \"class/KibblesTasty; Inventor.json\"\n        \"class/LaserLlama; Alternate Barbarian.json\"\n        \"class/Matthew Mercer; Blood Hunter (2022).json\"\n        \"class/badooga; Badooga's Psion.json\"\n        \"collection/Arcana Games; Arkadia.json\"\n        \"collection/Darrington Press; Tal’Dorei Campaign Setting Reborn.json\"\n        \"collection/Ghostfire Gaming; Grim Hollow - The Monster Grimoire.json\"\n        \"collection/Jasmine Yang; Hamund's Herbalism Handbook.json\"\n        \"collection/Keith Baker; Exploring Eberron.json\"\n        \"collection/Kobold Press; Deep Magic 14 Elemental Magic.json\"\n        \"collection/Kobold Press; Deep Magic.json\"\n        \"collection/Loot Tavern; Heliana's Guide To Monster Hunting.json\"\n        \"collection/MCDM Productions; The Talent and Psionics Open Playtest Round 2.json\"\n        \"collection/Mage Hand Press; Valda's Spire of Secrets.json\"\n        \"creature/Dragonix; Monster Manual Expanded III.json\"\n        \"creature/Kobold Press; Creature Codex.json\"\n        \"creature/Kobold Press; Tome of Beasts 2.json\"\n        \"creature/Kobold Press; Tome of Beasts.json\"\n        \"creature/MCDM Productions; Flee, Mortals! preview.json\"\n        \"creature/MCDM Productions; Flee, Mortals!.json\"\n        \"creature/Nerzugal Role-Playing; Nerzugal's Extended Bestiary.json\"\n        \"deity/Frog God Games; The Lost Lands.json\"\n        \"optionalfeature/laserllama; Laserllama's Exploit Compendium.json\"\n        \"race/Middle Finger of Vecna; Archon.json\"\n        \"spell/LaserLlama; LaserLlama's Compendium of Spells.json\"\n        \"subclass/LaserLlama; Druid Circles.json\"\n        )\n\n        for i in \"${paths[@]}\"; do\n          echo \"$i\"\n          url=${i// /%20}\n          echo \"$url\"\n          curl -s -S -L -o \"sources/5e-homebrew/$i\" \"https://raw.githubusercontent.com/TheGiddyLimit/homebrew/refs/heads/master/${url}\"\n        done\n\n        echo \"🔹 5eTools: Download Unearthed Arcana\"\n\n        # Don't grab all of unearthed arcana\n        mkdir -p sources/5e-unearthed-arcana/collection\n\n        paths=(\n        \"collection/Unearthed Arcana - Downtime.json\"\n        \"collection/Unearthed Arcana - Encounter Building.json\"\n        \"collection/Unearthed Arcana - Into the Wild.json\"\n        \"collection/Unearthed Arcana - Quick Characters.json\"\n        \"collection/Unearthed Arcana - Traps Revisited.json\"\n        \"collection/Unearthed Arcana - When Armies Clash.json\"\n        \"collection/Unearthed Arcana 2022 - Character Origins.json\"\n        \"collection/Unearthed Arcana 2022 - Expert Classes.json\"\n        \"collection/Unearthed Arcana 2022 - The Cleric and Revised Species.json\"\n        \"collection/Unearthed Arcana 2023 - Bastions and Cantrips.json\"\n        \"collection/Unearthed Arcana 2023 - Druid & Paladin.json\"\n        \"collection/Unearthed Arcana 2023 - Player's Handbook Playtest 5.json\"\n        \"collection/Unearthed Arcana 2023 - Player's Handbook Playtest 6.json\"\n        \"collection/Unearthed Arcana 2023 - Player's Handbook Playtest 7.json\"\n        )\n\n        for i in \"${paths[@]}\"; do\n          echo \"$i\"\n          url=${i// /%20}\n          echo \"$url\"\n          curl -s -S -L -o \"sources/5e-unearthed-arcana/$i\" \"https://raw.githubusercontent.com/TheGiddyLimit/unearthed-arcana/refs/heads/master/${url}\"\n        done\n\n        echo \"🔹 Pf2eTools Download ${LATEST_PF2E_VERSION}\"\n\n        # sparse clone of src\n        mkdir -p sources/Pf2eTools\n        git clone --quiet --depth=1 -b ${LATEST_PF2E_VERSION} \\\n            -c advice.detachedHead=false \\\n            --no-checkout \\\n            https://github.com/Pf2eToolsOrg/Pf2eTools.git sources/Pf2eTools\n        pushd sources/Pf2eTools\n          git sparse-checkout init\n          git sparse-checkout set data img\n          git checkout ${LATEST_VERSION}\n          rm -rf .git\n          rm -f *.png\n          rm -f *.html\n          rm -f *.xml\n          find img -type f | while read FILE; do echo > \"$FILE\"; done\n        popd\n\n        ls -al sources\n        du -sh sources\n\n    - name: Always Save Cache\n      id: test_data_save\n      if: always() && steps.test_data_check.outputs.cache-hit != 'true'\n      uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4\n      with:\n        key: ${{ steps.test_data.outputs.cache_key }}\n        path: sources\n"
  },
  {
    "path": ".github/actions/native-data-cache/action.yml",
    "content": "name: 'Fetch Tools data cache'\ndescription: 'Fetch cache using known key'\n\ninputs:\n  cache_key:\n    description: Key for data cache\n    required: true\n\nruns:\n  using: \"composite\"\n  steps:\n\n    - id: cache_restore\n      uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0\n      with:\n        path: sources\n        key: ${{ inputs.cache_key }}\n        fail-on-cache-miss: true\n        enableCrossOsArchive: true\n        restore-keys: |\n          \"TestData-\"\n"
  },
  {
    "path": ".github/augment-release.sh",
    "content": "#!/usr/bin/env bash\n\nif [ -z \"$1\" ]; then\n  echo \"Specify target version\"\n  exit 1\nfi\nif [ -z \"$JRELEASER_GITHUB_TOKEN\" ]; then\n  echo \"Specify JRELEASER_GITHUB_TOKEN\"\n  exit 1\nfi\n\nexport JRELEASER_PROJECT_VERSION=$1\n\ngit fetch --all\ngit checkout ${JRELEASER_PROJECT_VERSION}\n\n./mvnw clean package -Dnative -DskipTests -DskipITs \njreleaser assemble -s archive --select-current-platform --output-directory target\njreleaser release --output-directory target --select-current-platform --exclude-distribution ttrpg-convert\n\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# Please see the documentation for all configuration options:\n# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates\n\nversion: 2\nupdates:\n  - package-ecosystem: \"maven\"\n    directory: \"/\" # Location of package manifests\n    schedule:\n      interval: \"weekly\"\n    open-pull-requests-limit: 3\n    cooldown:\n      default-days: 30\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n    cooldown:\n      default-days: 30\n"
  },
  {
    "path": ".github/project.yml",
    "content": "name: TTRPG Convert CLI\nrelease:\n  current-version: 3.3.1\n  next-version: 3.3.1\n  snapshot-version: 399-SNAPSHOT\nbuild:\n  artifact: ttrpg-convert-cli\njitpack:\n  group: com.github.ebullient\n  artifact: ttrpg-convert-cli\ntoggle: false\n"
  },
  {
    "path": ".github/workflows/cache.yml",
    "content": "name: Refresh cache\non:\n  schedule:\n    - cron: '0 2 * * 1'  # Mondays at 2 AM UTC\n  workflow_dispatch:\n\njobs:\n  clear_cache:\n    runs-on: ubuntu-latest\n    permissions:\n      actions: write\n    steps:\n      - name: Delete old caches (except TestData)\n        env:\n          GH_TOKEN: ${{ github.token }}\n        run: |\n          cutoff=$(date -d '14 days ago' +%s)\n  \n          gh api \\\n            -H \"Accept: application/vnd.github+json\" \\\n            \"/repos/${{ github.repository }}/actions/caches?per_page=100\" \\\n            --paginate \\\n            --jq '.actions_caches[]' | \\\n          jq -r --arg cutoff \"$cutoff\" '\n            select(.key | startswith(\"TestData-\") | not) |\n            select((.last_accessed_at | sub(\"\\\\..*Z$\"; \"Z\") | fromdateiso8601) < ($cutoff | tonumber)) |\n            \"\\(.id) \\(.key)\"\n          ' | while read cache_id cache_key; do\n            if [ -n \"$cache_id\" ]; then\n              echo \"Deleting cache $cache_id ($cache_key)\"\n              gh api --method DELETE \\\n                -H \"Accept: application/vnd.github+json\" \\\n                \"/repos/${{ github.repository }}/actions/caches/$cache_id\" || true\n            fi\n          done\n\n  cache:\n    runs-on: ubuntu-latest\n    outputs:\n      cache_key: ${{ steps.data_cache.outputs.cache_key }}\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - id: data_cache\n        uses: ./.github/actions/data-cache\n\n      - uses: ./.github/actions/native-data-cache\n        with:\n          cache_key: ${{ steps.data_cache.outputs.cache_key }}\n"
  },
  {
    "path": ".github/workflows/maven.yml",
    "content": "name: Main Maven build\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - main\n    paths:\n      - .github/workflows/maven.yml\n      - .github/project.yml\n      - \"**.xml\"\n      - \"*.yml\"\n      - \"src/**\"\n      - \"ide-config\"\n\nenv:\n  JAVA_VERSION: 17\n  JAVA_DISTRO: temurin\n  NATIVE_JAVA_VERSION: 24\n  GRAALVM_DIST: graalvm-community\n  GH_BOT_EMAIL: \"41898282+github-actions[bot]@users.noreply.github.com\"\n  GH_BOT_NAME: \"GitHub Action\"\n\npermissions: read-all\n\njobs:\n  main_root:\n    runs-on: ubuntu-latest\n    outputs:\n      is_main: ${{ steps.is_main_root.outputs.is_main }}\n    steps:\n      - name: Echo a message\n        id: is_main_root\n        if: github.ref == 'refs/heads/main' && github.repository == 'ebullient/ttrpg-convert-cli'\n        run: |\n          echo \"This is the main branch of 'ebullient/ttrpg-convert-cli'\"\n          echo \"is_main=true\" >> $GITHUB_OUTPUT\n\n  metadata:\n    uses: ebullient/workflows/.github/workflows/java-snap-metadata.yml@65cfcd66420f2a8032c82e84bd6688a1a64713a9 # 2.0.1\n\n  build:\n    runs-on: ubuntu-latest\n    needs: [main_root, metadata]\n    outputs:\n      cache_key: ${{ steps.data_cache.outputs.cache_key }}\n\n    permissions:\n      contents: write\n      actions: write\n      security-events: write\n\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          fetch-depth: 0\n          fetch-tags: false\n\n      - uses: ./.github/actions/data-cache\n        id: data_cache\n\n      - name: Build with Maven\n        uses: ebullient/workflows/.github/actions/maven-build@65cfcd66420f2a8032c82e84bd6688a1a64713a9 # 2.0.1\n        with:\n          artifact: ${{ needs.metadata.outputs.artifact }}\n          version: ${{ needs.metadata.outputs.snapshot }}\n          java-version: ${{ env.JAVA_VERSION }}\n          java-distribution: ${{ env.JAVA_DISTRO }}\n\n      - name: Push changes to files\n        if: needs.main_root.outputs.is_main\n        uses: ebullient/workflows/.github/actions/push-changes@65cfcd66420f2a8032c82e84bd6688a1a64713a9 # 2.0.1\n        with:\n          files: \"README.md README-WINDOWS.md docs\"\n\n  native-build:\n    needs: [metadata, build]\n    name: Build ${{ matrix.os }} binary\n    runs-on: ${{ matrix.os }}\n    concurrency:\n      group: native-${{ matrix.os }}-${{ github.ref }}\n      cancel-in-progress: true\n    strategy:\n      fail-fast: false\n      max-parallel: 3\n      matrix:\n        os: [macos-15-intel, macos-latest, windows-latest, ubuntu-latest]\n    permissions:\n      contents: read\n      actions: write\n\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          fetch-depth: 1\n\n      - uses: ./.github/actions/native-data-cache\n        with:\n          cache_key: ${{ needs.build.outputs.cache_key }}\n\n      - name: Native build with Maven\n        uses: ebullient/workflows/.github/actions/native-build@65cfcd66420f2a8032c82e84bd6688a1a64713a9 # 2.0.1\n        with:\n          artifact: ${{ needs.metadata.outputs.artifact }}\n          version: ${{ needs.metadata.outputs.snapshot }}\n          native-java-version: ${{ env.NATIVE_JAVA_VERSION }}\n          distribution: ${{ env.GRAALVM_DIST }}\n          matrix-os: ${{ matrix.os }}\n\n  snap-release:\n    needs: [main_root, metadata, build, native-build]\n    if: needs.main_root.outputs.is_main\n    uses: ebullient/workflows/.github/workflows/java-snapshot.yml@65cfcd66420f2a8032c82e84bd6688a1a64713a9 # 2.0.1\n    permissions:\n      contents: write\n    with:\n      artifact: ${{ needs.metadata.outputs.artifact }}\n      snapshot: ${{ needs.metadata.outputs.snapshot }}\n    secrets: inherit\n"
  },
  {
    "path": ".github/workflows/pull-request.yml",
    "content": "name: PR Maven Build\non:\n  pull_request:\n    paths:\n      - \"**.xml\"\n      - \"*.yml\"\n      - \"src/**\"\n      - \"ide-config\"\n\nenv:\n  JAVA_VERSION: 17\n  JAVA_DISTRO: temurin\n  NATIVE_JAVA_VERSION: 23\n  GRAALVM_DIST: graalvm-community\n  GH_BOT_EMAIL: \"41898282+github-actions[bot]@users.noreply.github.com\"\n  GH_BOT_NAME: \"GitHub Action\"\n\npermissions:\n  contents: read\n  actions: read\n\njobs:\n  metadata:\n    uses: ebullient/workflows/.github/workflows/java-snap-metadata.yml@65cfcd66420f2a8032c82e84bd6688a1a64713a9 # 2.0.1\n\n  build:\n    runs-on: ubuntu-latest\n    needs: [metadata]\n    outputs:\n      cache_key: ${{ steps.data_cache.outputs.cache_key }}\n\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - uses: ./.github/actions/data-cache\n        id: data_cache\n        with:\n          failIfMissing: true\n\n      - name: Build with Maven\n        uses: ebullient/workflows/.github/actions/maven-build@65cfcd66420f2a8032c82e84bd6688a1a64713a9 # 2.0.1\n        with:\n          artifact: ${{ needs.metadata.outputs.artifact }}\n          version: ${{ needs.metadata.outputs.snapshot }}\n          java-version: ${{ env.JAVA_VERSION }}\n          java-distribution: ${{ env.JAVA_DISTRO }}\n\n  native-build:\n    runs-on: ubuntu-latest\n    needs: [metadata, build]\n\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          fetch-depth: 1\n\n      - uses: ./.github/actions/native-data-cache\n        with:\n          cache_key: ${{ needs.build.outputs.cache_key }}\n\n      - name: Native build with Maven\n        uses: ebullient/workflows/.github/actions/native-build@65cfcd66420f2a8032c82e84bd6688a1a64713a9 # 2.0.1\n        with:\n          artifact: ${{ needs.metadata.outputs.artifact }}\n          version: ${{ needs.metadata.outputs.snapshot }}\n          native-java-version: ${{ env.NATIVE_JAVA_VERSION }}\n          distribution: ${{ env.GRAALVM_DIST }}\n          matrix-os: ubuntu-latest\n\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Create a release\n\non:\n  workflow_dispatch:\n    inputs:\n      semver:\n        description: \"New version or major, minor, patch, project\"\n        default: \"patch\"\n        required: true\n        type: string\n      retry:\n        description: \"Retry release (clear created tag)\"\n        default: false\n        required: true\n        type: boolean\n      dry_run:\n        description: \"Test capabilities, do not release\"\n        default: false\n        required: true\n        type: boolean\n\nenv:\n  JAVA_VERSION: 17\n  JAVA_DISTRO: temurin\n  GRAALVM_DIST: graalvm-community\n  GH_BOT_EMAIL: \"41898282+github-actions[bot]@users.noreply.github.com\"\n  GH_BOT_NAME: \"GitHub Action\"\n  CI: true\n\npermissions: read-all\n\njobs:\n  main-root:\n    runs-on: ubuntu-latest\n    if: github.ref == 'refs/heads/main' && github.repository == 'ebullient/ttrpg-convert-cli'\n    steps:\n      - name: Echo a message\n        id: is-main-root\n        run: echo \"This is the main branch of 'ebullient/ttrpg-convert-cli'\"\n\n  build_tag:\n    runs-on: ubuntu-latest\n    needs: main-root\n    outputs:\n      artifact: ${{ steps.git-commit-tag.outputs.artifact }}\n      snapshot: ${{ steps.git-commit-tag.outputs.snapshot }}\n      version: ${{ steps.git-commit-tag.outputs.next }}\n      current: ${{ steps.git-commit-tag.outputs.current }}\n    permissions:\n      contents: write\n      actions: write\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      # Fetches all tags for the repo\n      - name: Fetch tags\n        run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*\n\n      - name: Set up JDK ${{ env.JAVA_VERSION }}\n        uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0\n        with:\n          java-version: ${{ env.JAVA_VERSION }}\n          distribution: ${{ env.JAVA_DISTRO }}\n          cache: maven\n\n      - name: Commit and Tag release\n        id: git-commit-tag\n        env:\n          INPUT: ${{ inputs.semver }}\n          RETRY: ${{ inputs.retry }}\n          DRY_RUN: ${{ inputs.dry_run }}\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          COMMIT_PATHS: \"README-WINDOWS.md docs examples\"\n          TEXT_REPLACE: \"README.md README-WINDOWS.md docs/alternateRun.md\"\n        run: |\n          gh repo clone ebullient/workflows shared-workflows -- --depth=1\n          git config user.name ${{ env.GH_BOT_NAME }}\n          git config user.email ${{ env.GH_BOT_EMAIL }}\n\n          ## Check and update version, build, and create tag\n          ## outputs: CURRENT SNAPSHOT ARTIFACT NEXT\n          . ./shared-workflows/version-build-tag.sh\n\n          cp -R src/main/resources/templates default\n          zip -r target/${ARTIFACT}-${NEXT}-examples.zip docs examples default\n\n      - name: Upload ${{ steps.git-commit-tag.outputs.artifact }}-${{ steps.git-commit-tag.outputs.next }}-runner.jar\n        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        id: upload-jar\n        with:\n          name: artifacts-runner\n          path: target/${{ steps.git-commit-tag.outputs.artifact }}-${{ steps.git-commit-tag.outputs.next }}-runner.jar\n\n      - name: Upload ${{ steps.git-commit-tag.outputs.artifact }}-${{ steps.git-commit-tag.outputs.next }}-examples.zip\n        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        id: upload-zip\n        with:\n          name: artifacts-examples\n          path: target/${{ steps.git-commit-tag.outputs.artifact }}-${{ steps.git-commit-tag.outputs.next }}-examples.zip\n\n  native-binaries:\n    needs: build_tag\n    uses: ebullient/workflows/.github/workflows/java-release-native-binaries.yml@65cfcd66420f2a8032c82e84bd6688a1a64713a9 # 2.0.1\n    permissions:\n      contents: read\n      actions: write\n    with:\n      version: ${{ needs.build_tag.outputs.version }}\n    secrets: inherit\n\n  release:\n    needs: [build_tag, native-binaries]\n    uses: ebullient/workflows/.github/workflows/jreleaser-release.yml@65cfcd66420f2a8032c82e84bd6688a1a64713a9 # 2.0.1\n    permissions:\n      contents: write\n      actions: write\n      discussions: write\n    with:\n      dry_run: ${{ inputs.dry_run }}\n      extras: \"${{ needs.build_tag.outputs.artifact }}-${{ needs.build_tag.outputs.version }}-examples.zip\"\n      version: ${{ needs.build_tag.outputs.version }}\n    secrets: inherit\n\n  release-prep-next:\n    needs: [build_tag, release]\n    uses: ebullient/workflows/.github/workflows/java-release-prep-next.yml@65cfcd66420f2a8032c82e84bd6688a1a64713a9 # 2.0.1\n    permissions:\n      contents: write\n    with:\n      dry_run: ${{ inputs.dry_run }}\n      snapshot: ${{ needs.build_tag.outputs.snapshot }}\n    secrets: inherit\n"
  },
  {
    "path": ".github/workflows/scorecard.yml",
    "content": "# This workflow uses actions that are not certified by GitHub. They are provided\n# by a third-party and are governed by separate terms of service, privacy\n# policy, and support documentation.\n\nname: Scorecard supply-chain security\non:\n  workflow_dispatch:\n  # For Branch-Protection check. Only the default branch is supported. See\n  # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection\n  branch_protection_rule:\n  # To guarantee Maintained check is occasionally updated. See\n  # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained\n  schedule:\n    - cron: '31 10 * * 4'\n  push:\n    branches: [ \"main\" ]\n\n# Declare default permissions as read only.\npermissions: read-all\n\njobs:\n  analysis:\n    name: Scorecard analysis\n    runs-on: ubuntu-latest\n    permissions:\n      # Needed to upload the results to code-scanning dashboard.\n      security-events: write\n      # Needed to publish results and get a badge (see publish_results below).\n      id-token: write\n      # Uncomment the permissions below if installing in a private repository.\n      # contents: read\n      # actions: read\n\n    steps:\n      - name: \"Checkout code\"\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n\n      - name: \"Run analysis\"\n        uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3\n        with:\n          results_file: results.sarif\n          results_format: sarif\n          # (Optional) \"write\" PAT token. Uncomment the `repo_token` line below if:\n          # - you want to enable the Branch-Protection check on a *public* repository, or\n          # - you are installing Scorecard on a *private* repository\n          # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat.\n          # repo_token: ${{ secrets.SCORECARD_TOKEN }}\n\n          # Public repositories:\n          #   - Publish results to OpenSSF REST API for easy access by consumers\n          #   - Allows the repository to include the Scorecard badge.\n          #   - See https://github.com/ossf/scorecard-action#publishing-results.\n          # For private repositories:\n          #   - `publish_results` will always be set to `false`, regardless\n          #     of the value entered here.\n          publish_results: true\n\n      # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF\n      # format to the repository Actions tab.\n      - name: \"Upload artifact\"\n        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        with:\n          name: SARIF file\n          path: results.sarif\n          retention-days: 5\n\n      # Upload the results to GitHub's code scanning dashboard.\n      - name: \"Upload to code-scanning\"\n        uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1\n        with:\n          sarif_file: results.sarif\n"
  },
  {
    "path": ".github/workflows/test-data.yml",
    "content": "name: Test with data (scheduled)\non:\n  schedule:\n    - cron: \"7 9 * * */5\"\n\n  workflow_dispatch:\n\nenv:\n  JAVA_VERSION: 17\n  JAVA_DISTRO: temurin\n  NATIVE_JAVA_VERSION: 23\n  GRAALVM_DIST: graalvm-community\n  FAIL_ISSUE: 140\n\npermissions: read-all\n\njobs:\n  metadata:\n    uses: ebullient/workflows/.github/workflows/java-snap-metadata.yml@65cfcd66420f2a8032c82e84bd6688a1a64713a9 # 2.0.1\n\n  build:\n    runs-on: ubuntu-latest\n    needs: [metadata]\n    outputs:\n      cache_key: ${{ steps.data_cache.outputs.cache_key }}\n\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - id: data_cache\n        uses: ./.github/actions/data-cache\n\n      - name: Build with Maven\n        uses: ebullient/workflows/.github/actions/maven-build@65cfcd66420f2a8032c82e84bd6688a1a64713a9 # 2.0.1\n        with:\n          artifact: ${{ needs.metadata.outputs.artifact }}\n          version: ${{ needs.metadata.outputs.snapshot }}\n          java-version: ${{ env.JAVA_VERSION }}\n          java-distribution: ${{ env.JAVA_DISTRO }}\n\n  native-build:\n    runs-on: ubuntu-latest\n    needs: [metadata, build]\n\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          fetch-depth: 1\n\n      - uses: ./.github/actions/native-data-cache\n        with:\n          cache_key: ${{ needs.build.outputs.cache_key }}\n\n      - name: Native build with Maven\n        uses: ebullient/workflows/.github/actions/native-build@65cfcd66420f2a8032c82e84bd6688a1a64713a9 # 2.0.1\n        with:\n          artifact: ${{ needs.metadata.outputs.artifact }}\n          version: ${{ needs.metadata.outputs.snapshot }}\n          native-java-version: ${{ env.NATIVE_JAVA_VERSION }}\n          distribution: ${{ env.GRAALVM_DIST }}\n          matrix-os: ubuntu-latest\n\n  report-native-build:\n\n    name: Report errors\n    runs-on: ubuntu-latest\n    if: ${{ failure() }}\n    needs: [build, native-build]\n    permissions:\n      contents: read\n      issues: write\n\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          fetch-depth: 1\n\n      - id: gh-issue\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          gh issue comment ${{ env.FAIL_ISSUE }} --body \"[Maven build failed: ${{ github.run_id }}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})\"\n          gh issue reopen ${{ env.FAIL_ISSUE }}\n"
  },
  {
    "path": ".github/workflows/website.yml",
    "content": "name: Update docs\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - main\n    paths:\n      - \"**.md\"\n\npermissions: read-all\n\njobs:\n  bump:\n    runs-on: ubuntu-latest\n    if: github.ref == 'refs/heads/main' && github.repository == 'ebullient/ttrpg-convert-cli'\n    steps:\n      - name: Bump website\n        env:\n          GH_TOKEN: ${{ secrets.EBULLIENT_PAT }}\n        run: |\n          gh workflow run -R ebullient/ebullient.github.io gh-pages.yml\n"
  },
  {
    "path": ".gitignore",
    "content": "sources\nexcludes.json\npublish\ncompendium\nartifacts\ndefault\n\n#Maven\ntarget/\npom.xml.tag\npom.xml.releaseBackup\npom.xml.versionsBackup\nrelease.properties\n\n# Eclipse\n.project\n.classpath\n.settings/\nbin/\n\n# IntelliJ\n.idea\n*.ipr\n*.iml\n*.iws\n\n# NetBeans\nnb-configuration.xml\n\n# Visual Studio Code\n.vscode\n.factorypath\n\n# OSX\n.DS_Store\n\n# Vim\n*.swp\n*.swo\n\n# patch\n*.orig\n*.rej\n\n# Local environment\n.env\ntmp*\n\n.flattened*\npom.xml.tag\npom.xml.releaseBackup\npom.xml.versionsBackup\nrelease.properties\n*.log\n\nJRELEASER_VERSION\njreleaser-cli.jar\nnative-image-Linux-logs\nnative-image-Windows-logs\nnative-image-macOS-logs\n.quarkus/cli/plugins/quarkus-cli-catalog.json\n\n# Javadoc things\n/javadoc.*\n/options\n/packages\nttrpg-convert.out.*\n.claude/settings.local.json\n"
  },
  {
    "path": ".markdownlint.yaml",
    "content": "MD007:\n  indent: 4\nMD013: false\nMD033:\n  allowed_elements:\n    - blockquote\n"
  },
  {
    "path": ".mvn/wrapper/.gitignore",
    "content": "maven-wrapper.jar\n"
  },
  {
    "path": ".mvn/wrapper/maven-wrapper.properties",
    "content": "wrapperVersion=3.3.4\ndistributionType=only-script\ndistributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip\n"
  },
  {
    "path": "AGENTS.md",
    "content": "This file provides guidance to AI Agents working with code in this repository.\n\n**For complete build commands, architecture overview, and development guidelines, see [CONTRIBUTING.md](CONTRIBUTING.md).**\n\n## Your Role\n\nAct as a pair programming partner with these responsibilities:\n\n- **REVIEW THOROUGHLY**: Use file system access when available\n    - Analyze information flow and cross-module interactions\n    - ASK FOR CLARIFICATION if implementation choices are unclear\n- **BE EFFICIENT**: Be succinct and concise, don't waste tokens\n- **RESPECT PRIVACY**: Do not read .env* files unless instructed to do so\n- **NO SPECULATION**: Never make up code unless asked\n\n## Project Overview\n\nThis is a Quarkus-based CLI tool that converts TTRPG JSON data (from 5eTools and Pf2eTools) into markdown files optimized for Obsidian.md.\n\n## Essential Commands\n\n**Build and format code (required before commits):**\n```bash\n./mvnw install\n```\n\n**Run tests:**\n```bash\n./mvnw test\n```\n\n**Run a specific test:**\n```bash\n./mvnw test -Dtest=ClassName#methodName\n```\n\n**Format code only:**\n```bash\n./mvnw process-sources\n```\n\n## Understanding the Codebase\n\n**Before making changes, read:**\n- Architecture and control flow: [CONTRIBUTING.md § Notes on control flow](CONTRIBUTING.md#notes-on-control-flow)\n- Unconventional conventions: [CONTRIBUTING.md § Unconventional conventions](CONTRIBUTING.md#unconventional-conventions)\n\n**Key points:**\n- This project uses Jackson with raw types (`JsonNode`, `ArrayNode`, `ObjectNode`) intentionally\n- Parsing uses interface hierarchies with default methods\n- Prefer enum-based field access (Pf2e pattern) over string keys\n- Data flow: Index → Prepare → Render\n- Templates use Qute engine\n\n## Key Development Principles\n\n- **Follow existing patterns**: Find similar functions in the same module (5e vs Pf2e) and emulate them\n- **Understand the JSON quirks**: Source data has union types and dynamic structures from JavaScript\n- **Test with live data**: Add 5eTools/Pf2eTools to `sources/` directory for testing\n- **Respect architectural boundaries**: Use interface default methods to share parsing logic\n\n## Testing\n\nKey test files to understand:\n- `Tools5eDataConvertTest` - CLI launch tests with different parameters\n- `dev.ebullient.convert.tools.dnd5e.CommonDataTests` - Shared test cases\n- `JsonDataNoneTest` (SRD), `JsonDataSubsetTest`, `JsonDataTest` (all data)\n- `*IT` tests run against final artifact (not from IDE)\n\n## Commit Guidelines\n\n- Rebase commits (no merge commits)\n- Use [gitmoji](https://gitmoji.dev/) at the beginning (actual emoji, not text)\n- Be strict about: 🐛 (bugs), ✨ (new features), ♻️ (refactoring), 👷 (CI/build)\n- Use ✨ for features that should be in CHANGELOG\n- Use 🔥💥 for breaking changes that should be in CHANGELOG\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n[examples/config]: examples/config\n[ex-snippets]: examples/css-snippets\n[ex-templates]: examples/templates\n[def-templates]: src/main/resources/templates/\n\n**Note:** Entries marked with \"🔥\" indicate crucial or breaking changes that might affect your current setup.\n\n## 3.3.0 Reference-based filtering and split rules\n\n- ✨ Filter table notes by reference: only emit tables linked from included content (`onlyReferencedTables`); resolves #862\n- ✨ Filter legendary groups by monster references; resolves #718\n- ✨ Option to break rules into separate documents (`splitRules`); resolves #781\n- 🐛 Use HTML to render complex tables; resolves #811\n- 🐛 Resolve reprint aliases before adding spell references; resolves #857\n- 🐛 Fix reprinted subclass spell list merging and wrong qualifiers; resolves #859\n- 🐛 Resolve reprinted class source in subclass link generation; refs #779\n- 🐛 Render psionic focus and modes in template; resolves #839\n- 🐛 Fix image floating (center, left); resolves #853\n- 🐛 Fix duplicated item properties\n- 🐛 Remove length limit for captions; resolves #762\n\n## 3.2.5 Races or species?\n\n- ✨ Add option to emit races as species\n- ✨ Lots of extra spell fields added to templates\n- 🐛 Resolve {{resist}} template values in bestiary\n- 🐛 Add subclass references for dunamancy spells\n- 🐛 Fix template vaultPath (#837)\n- 🐛 Handle alternate casting times (#827)\n\n## 3.2.2 New template extensions\n\n- 🎨 Additional template extensions; recompiled/updated CSS for images\n    - `{resource.<attribute>.first}` -- retrieve the first element in a list\n    - `{resource.<attribute>.skipFirst}` -- skip the first element of a list\n    - `{resource.<attribute>.size}` -- return the size of a list\n    - `{resource.<attribute>.quotedEscaped}` -- Escape double quotes in a string (YAML/frontmatter safe)\n- ✨ Add featCategory prerequisite\n- 🐛 Fix handling of special AC, spellcasting, and cult links\n\n## 3.2.1 Airships and gear\n\n- ✨ 5e: Elemental Airship support\n- ✨ Added gear to the 5e monster template\n\n## 3.1.6 / 3.2.0: 🔥 Changes to template aliases\n\n- 🐛 5e: Add membership prerequisite (feats, etc.)\n- 🔥 `aliases` template attribute: new for 5e, changed for Pf2e\n\n    Instead of using `{resource.name}` as a single alias, iterate over the group:\n\n    ```md\n    aliases:\n    {#each resource.aliases}\n    - {it}\n    {/each}\n    ```\n\n## 3.1.5: Lots of goodies\n\n- ✨ Add feat categories handling\n- ✨ Sort class lists for spells; add feats, races, and backgrounds lists\n- ✨ Linkify senses in monster statblocks (#754)\n- ✨ Mythic Actions; Gear attributes; Trait entries in statblocks\n- ✨ Additional template extensions for case munging\n- ✨ Template methods for 2024 template\n- 🐛 Include group name for additional spells (#784)\n- 🐛 No table roll string when dice disabled (#761)\n- 🐛 Make singular dice roll formats consistent with source material\n- 🐛 Fix footnotes in class progression (#747)\n- 🐛 Remove duplicate optional features (#744)\n- 🎨 cssclasses as array; two-column layout for indexes\n- ♻️ Use full feature type titles for homebrew optional features\n\n## 🔥💥 3.1.0: Accommodating 2024 rules (XMM and SRD)\n\n- 💥 **Monster spellcasting traits**: 2024 monster statblocks interleave spellcasting behaviors in trait categories.\n    - trait-like attributes (`trait`, `action`, `bonusAction`, `reaction`, `legendary`) will already include spellcasting information. For 2014 rules, the information will be included in the trait attribute.\n    - `spellcasting` will now always return empty (no breakage, but also no output)\n    - `rawSpellcasting` can be used to retrieve raw [SpellCasting attributes](./docs/templates/dnd5e/QuteMonster/Spellcasting.md).\n        - `fixed` defines spells with a fixed duration (simple list): `constant`, `ritual` and `will`\n        - `variable` (varying frequencies): `charges`, `daily`, `legendary`, `monthly`, `recharge`, `rest`, `restLong`, `weekly`, `yearly`\n        - `daily` and `will` have been removed (incorporated in `variable` and `fixed`, respectively).\n- ✨ **Additional item fields**: Item variants have a fistful of additional fields: `bonusAc`, `bonusWeapon`, `bonusWeaponAttack`, `bonusWeaponDamage`, `bonusWeaponCritDamage`, `bonusSpellAttack`, `bonusSpellDamage`, `bonusSpellSaveDc`, `bonusSavingThrow`, `bonusAbilityCheck`, `bonusProficiencyBonus`, `bonusSavingThrowConcentration`\n\n## 🔥✨ 3.0.0: Moving the things\n\nSupport for the 2024 ruleset caused a lot of ripples. There is nothing small about this release.\n\n- *Specifying Sources*\n    - Removed `-s` option. Sources must be specified in config files.\n    - The [Source Map](./docs/sourceMap.md) for 5e sources now includes `book` or `adventure` status\n    - Combined `from` and `full-source` into unified `sources` key with four types:\n\n        ```json\n        {\n            \"sources\": {\n                \"reference\": [...],  // Reference-only content\n                \"book\": [...],       // Complete book content\n                \"adventure\": [...],  // Complete adventure content \n                \"homebrew\": [...]    // Custom content\n            }\n        }\n        ```\n\n    - *Reprint handling*: CLI will default to the most recent version available for your selected sources.\n        - Use `include`/`exclude` config to override.\n        - For SRD/Basic rules for 2014 content, use: \"srd\", \"basicrules\"\n        - For SRD/Basic rules for 2024 content, use: \"srd52\", \"freerules2024\"\n- *Generated content*\n    - Changed Players Handbook directory to `players-handbook-2014`\n    - Dice roller uses the `|text` flag to make text more consistent\n      and readable (dice roll occurs on hover)\n    - Added a `lists` folder in the compendium:\n        - Lists of optional features\n        - Lists of spells by class, school, subclass, background, feat...\n- **5e Template updates**\n    - All templates:\n        - Added `books`: Abbreviated source book list\n        - Added `sourcesWithFootnote`: Primary source with additional sources as footnotes\n        - Image display (see [image examples](examples/templates/tools5e))\n            - hasImages - true if any images are present\n            - hasMoreImages - true if more than one image is present\n            - showAllImages - rendered wikilinks for all images. If there is more than one, they will be shown as a gallery.\n            - showMoreImages - rendered wikilinks for all but the first image. If there is more than one, they will be shown as a gallery.\n            - showPortraitImage - rendered wikilink for the first image with the '#right' anchor tag to float it to the right side.\n    - Added [Bastion template](docs/templates/dnd5e/QuteBastion/) (2024 rules)\n    - Class updates:\n        - Unified class progression tables\n        - [Hit Point Die](docs/templates/dnd5e/QuteClass/HitPointDie.md)\n        - [Starting Equipment](docs/templates/dnd5e/QuteClass/StartingEquipment.md)\n        - Improved [Multiclassing](docs/templates/dnd5e/QuteClass/Multiclassing.md)\n        - isClassic/classic: true if 2014 class\n        - isSidekick/sidekick: true if sidekick class\n    - Spells: Added `references` list linking to related content\n\nNote: Path changes may affect existing content links. All path updates support dual-edition content structure.\n\n## 🔖 ✨ 2.3.14: Improvements to Pathfinder rendering\n\n@miscoined has made significant contributions to improve support for Pf2e creatures (the bestiary). 🙏🎉💖\n\n## 🔖 🐛 2.3.13: Fallback paths for images\n\nIn the event you have a bad image reference and the copy fails, you can set a [fallback path](./docs/configuration.md#fallback-paths) for an image that should be used instead.\n\n```json\n\"images\": {\n    \"fallbackPaths\": {\n        \"img/bestiary/MM/Green Hag.jpg\": \"img/bestiary/MM/Green Hag.webp\"\n    },\n}\n```\n\n- The key (original path) must match what is used by the JSON Source.\n- The value (replacement path) should be either: a valid path to a local file or a valid URL to a remote file.\n\n## 🔖 🐛 2.3.11: Fixes for downloading images\n\nDownloading and/or copying [internal](./docs/configuration.md#copying-internal-images) or [external](./docs/configuration.md#copying-external-images) images has been a whack-a-mole exercise with path encodings across platforms and sources. Everything should finally be fixed now.\n\n## 🔖 ✨ 2.3.9: Fixes for Dice Roller and Statblock\n\nIf you use the Dice Roller plugin, `useDiceRoller: true` will add dice rolls to tables and in text.\n\nIf you are using the Fantasy Statblocks plugin to render your statblocks, you can now add `yamlStatblocks: true` to your configuration. This will strip backticks and other formatting related to dice rolls from statblock text (which will make Fantasy Statblocks happier), whether you have dice roller plugin enabled or not.\n\n## 🔖 ✨ 2.3.0: 5eTools moving to mirror2\n\nThe 5eTools Mirror now uses a separate repository for images. When you run `ttrpg-convert` against 5eTools content, images will not be copied into your vault by default. Links to external images will be used instead. This can greatly reduce the size of your vault, but you will need to be connected to the internet for images to render.\n\nSee [Copying \"internal\" images](./docs/configuration.md#copying-internal-images) for options on how to copy tokens and other item/spell/monster fluff images into your vault.\n\nSee [Copying \"external\" images](./docs/configuration.md#copying-external-images) to copy additional images referenced in general text into your vault.\n\n## 🔖 ✨ 2.2.12: 5e support for generic and magic item variants\n\nItems may have variants, which are defined as a list in the `variants` attribute.\n\n- Use `resource.variantAliases` to get a list of aliases for variants\n- Use `resource.variantSectionLinks` to get a list of links to variant sections\n- Iterate over the section list to generate sections (`##`) for each variant\n\nSee the following examples:\n\n- [Default `item2md.txt`](src/main/resources/templates/tools5e/item2md.txt)\n- [Example `examples/templates/tools5e/images-item2md.txt`](examples/templates/tools5e/images-item2md.txt)\n\n## 🔖 ✨ 2.2.5: New templates for decks (and cards), legendary groups, and psionics\n\n- **New templates**: `deck2md.txt`, `legendaryGroup2md.txt`, `psionic2md.txt`\n    - Decks, when present, will be generated under `compendium/decks`. Cards are part of decks.\n    - Legendary groups, when present, will be generated under `bestiary/legendary-groups`\n    - Psionics, when present, will be generated under `compendium/psionics`.\n- `feat2md.txt` is now also used for optional features.\n- The default `monster2md.txt` template has been updated to embed the legendary group.\n- CSS snippets have been updated to support legendary groups embedded in statblocks.\n\n## 🔖 🔥 2.1.0: File name and path changes, template docs and attribute changes\n\n1. 🔥 **Variant rules include the source in the file name**: this avoids duplicates (and there were some).\n2. 🔥 **5eTools changed the classification for some creatures**, which moves them in the bestiary. Specifically: the Four-armed troll is a giant (and not an npc), a river serpent is a monstrosity rather than a beast, and ogre skeletons and red dracoliches are both undead.\n3. 🔥 Better support for table rendering has superceded dedicated/hand-tended random name tables. All of the tables are still present, just in different units more directly related to source material.\n4. 🔥 **Change to monster template attributes:** Legendary group attributes have been simplified to `name` and `desc`, just like other traits. See the [default monster template](https://github.com/ebullient/ttrpg-convert-cli/blob/0736c3929a6d90fe01860692f487b8523b57e60d/src/main/resources/templates/tools5e/monster2md.txt#L80) for an example.\n\n> ***If you use the Templater plugin***, you can use [a templater script](https://github.com/ebullient/ttrpg-convert-cli/blob/main/migration/ttrpg-cli-renameFiles-5e-2.1.0.md) to **rename files in your vault before merging** with freshly generated content. View the contents of the template before running it, and adjust parameters at the top to match your Vault.\n\n✨ **New template documentation** is available in [docs](docs). Content is generated from javadoc in the various *.qute packages (for template-accessible fields and methods). It may not be complete.. PRs to improve it are welcome.\n\n## 🔖 🔥 2.0.0: File name and path changes, and styles\n\n1. 🔥 **A leading slash (`/`) is no longer used at the beginning of compendium and root paths**. This should allow you to move these two directories around more easily.\n    - I recommend that you keep the compendium and rules sections together as big balls of mud.\n    - If you do want to further move files around, do so from within obsidian, so obsidian can update its links.\n\n2. 🔥 **D&D 5e subclasses now use the source of the subclass in the file name**.\n\n    > ***If you use the Templater plugin***, you can use [a templater script](https://github.com/ebullient/ttrpg-convert-cli/blob/main/migration/ttrpg-cli-renameFiles-2.0.0.md) to rename files in your vault before merging with freshly generated content. View the contents of the template before running it, and adjust parameters at the top to match your Vault.\n\n3. 🎨 CSS styles for D&D 5e and Pathfinder are now available in `examples/css-snippets`.\n\n4. 📝 Admonitions are also available for import:\n    - 🎨 [admonitions-5e.json](examples/admonitions/admonitions-5e.json)\n    - [admonitions-pf2e-v3.json](examples/admonitions/admonitions-pf2e-v3.json)\n    - 🎨 [other-admonitions.json](examples/admonitions/other-admonitions.json)\n\n    Note: `admonitions-5e.json` and `other-admonitions.json` use colors from CSS snippets to adjust for light and dark mode.\n\n## 🔖 1.1.1: Dice roller in statblocks and text\n\nIf you are using the default templates and want to render dice rolls, set\n`useDiceRoller` to true to use dice roller strings when replacing dice `{@dice\n}`, and `{@damage }` strings. This can be set differently for either \"5e\" or\n\"pf2e\" configurations. Please note that if you are using a custom template\nand fantasy statblocks, you do **not** need to set the dice roller in your\nconfig. Fantasy statblocks will take care of the rendering itself.\n\nSee [examples/config][] for the general structure of config.\n\n## 🔖 1.1.0: Images for backgrounds, items, monsters, races, and spells\n\nThe conversion tool downloads fluff images into `img` directories within each type, e.g. `backgrounds/img` or `bestiary/aberration/img`. These images are unordered, and are not referenced in entry text. Templates must be modified to include them.\n\nTo display all images, you can do something like this:\n\n```md\n{#each resource.fluffImages}![{it.caption}]({it.path})  \n{/each}\n```\n\nNote that the line above ends with two spaces, which serves as a line break when you have strict line endings enabled. You may need something a little different to get things to wrap the way you want in the case that there are multiple images (which is infrequent for these types).\n\nYou can also use two separate blocks, such that the first image is used at the top of the document, and any others are included later:\n\n```md\n{#if resource.fluffImages && resource.fluffImages.size > 0 }{#let first=resource.fluffImages.get(0)}\n![{first.title}]({first.vaultPath}#right)  \n{/let}{/if}\n...\n{#each resource.fluffImages}{#if it_index != 0}![{it.caption}]({it.path}#center)  \n{/if}{/each}\n```\n\nNotice the `#right` and `#center` anchor tags in the example above. The following CSS snippet defines formatting for two anchor tags: `#right` (which will float the image to the right) and `#center` (which will display the image as a centered block).\n\n```css\n.json5e-background div[src$=\"#center\"],\n.json5e-item div[src$=\"#center\"],\n.json5e-monster div[src$=\"#center\"],\n.json5e-race div[src$=\"#center\"],\n.json5e-spell div[src$=\"#center\"] {\n  text-align: center;\n}\n.json5e-background div[src$=\"#center\"] img,\n.json5e-item div[src$=\"#center\"] img,\n.json5e-monster div[src$=\"#center\"] img,\n.json5e-race div[src$=\"#center\"] img,\n.json5e-spell div[src$=\"#center\"] img {\n  height: 300px;\n}\n.json5e-background div[src$=\"#right\"],\n.json5e-item div[src$=\"#right\"],\n.json5e-monster div[src$=\"#right\"],\n.json5e-race div[src$=\"#right\"],\n.json5e-spell div[src$=\"#right\"] {\n  float: right;\n  margin-left: 5px;\n}\n.json5e-background div[src$=\"#right\"] img,\n.json5e-item div[src$=\"#right\"] img,\n.json5e-monster div[src$=\"#right\"] img,\n.json5e-race div[src$=\"#right\"] img,\n.json5e-spell div[src$=\"#right\"] img {\n  height: 300px;\n}\n.rendered-widget .admonition-statblock-parent,\n.markdown-rendered .admonition-statblock-parent,\n.markdown-preview-section .admonition-statblock-parent {\n  clear: both;\n}\n```\n\nNotes:\n\n- I recommend constraining the image height (rather than the width) in your CSS snippet for images.\n- The above snippet also adds a `clear` setting to the admonition parent. Some text descriptions are shorter than the constrained image height. Setting `clear: both` on `admonition-parent` ensures that images floated to the right do not impact the `statblock` display.\n- This configuration is in the [compendium.css snippet][ex-snippets].\n- There is an example for each type in the [example templates directory][ex-templates] directory. Relevant file names start with `images-`.\n\n## 🔖  1.0.18: You can put more things in json input now\n\nUse `convert` to import source text for books and adventures that you own:\n\n```json\n  \"convert\": {\n    \"adventure\": [\n      \"WBtW\"\n    ],\n    \"book\": [\n      \"PHB\"\n    ]\n  }\n```\n\nSpecify templates in json:\n\n```json\n  \"template\": {\n    \"background\": \"path/to/template.txt\",\n  }\n```\n\nBe careful of paths here. Relative paths will be resolved depending on where the command is run from. Absolute paths will be machine specific (most likely). Use forward slashes for path segments, even if you're working on windows.\n\nYou can place this configuration one file or several, your choice.\n\n## 🔖  1.0.16: Sections in Spell text\n\nText for changes to spells at higher levels is added to spells a little differently depending on how complicated the spell is.\n\nSome spells effectively have subsections. Create or Destroy Water, from the PHB, has one subsection describing how water is created, and another describing how it is destroyed. In many layouts, there is just a bit of bold text to visually highlight this information. I've opted to make these proper sections (with a heading) instead, because you can then embed/transclude just the variant you want into your notes where that is relevant.\n\nIf a spell has sections, then \"At Higher Levels\" will be added as an additional section. Otherwise, it will be appended with `**At Higher Levels.**` as leading eyecatcher text.\n\nThe [default spell template (spell2md.txt)][def-templates] will test for sections in the spell text, and if so, now inserts a `## Summary` header above the Classes/Sources information, to ensure that the penultimate section can be embedded cleanly.\n\n## 🔖  1.0.15: Flowcharts, optfeature in text, styled rows\n\n- `optfeature` text is rendered (Tortle package)\n- `flowcharts` is rendered as a series of `flowchart` callouts  \n    Use the admonition plugin to create a custom `flowchart` callout with an icon of your choice.\n- The adventuring gear tables from the PHB have been corrected\n\n## 🔖  1.0.14: Ability Scores\n\nAs shown in [monster2md-scores.txt][ex-templates], you can now access ability scores directly to achieve alternate layouts in templates, for example:\n\n```md\n- STR: {resource.scores.str} `dice: 1d20 {resource.scores.strMod}`\n- DEX: {resource.scores.dex} `dice: 1d20 {resource.scores.dexMod}`\n- CON: {resource.scores.con} `dice: 1d20 {resource.scores.conMod}`\n- INT: {resource.scores.int} `dice: 1d20 {resource.scores.intMod}`\n- WIS: {resource.scores.wis} `dice: 1d20 {resource.scores.wisMod}`\n- CHA: {resource.scores.cha} `dice: 1d20 {resource.scores.chaMod}`\n```\n\n## 🔖 1.0.13: Item property tags are now sorted\n\nProperty tags on items are now sorted (not alphabetically) to stabilize their order in generated files. This should be a one-time bit of noise as you cross this release (using a version before to using some version after).\n\n## 🔖 🔥 1.0.12: File name and image reference changes\n\n### 🔥 File name changes\n\nEach file name will now contain an abbreviation of the primary source to avoid conflicts (for anything that does not come from phb, mm, dmg).\n\n***If you use the Templater plugin***, you can use [a templater script](https://github.com/ebullient/ttrpg-convert-cli/blob/main/migration/json5e-cli-renameFiles-1.0.12.md) to rename files in your vault before merging with freshly generated content. View the contents of the template before running it, and adjust parameters at the top as necessary.\n\n### 🔥 1.0.12: Deity symbols and Bestiary tokens\n\nSymbols and tokens have changed in structure. Custom templates will need a bit of adjustment.\n\nFor bestiary tokens:\n\n```md\n{#if resource.token}\n![{resource.token.caption}]({resource.token.path}#token){/if}\n```\n\nFor deities:\n\n```md\n{#if resource.image}\n![{resource.image.caption}]({resource.image.path}#symbol){/if}\n```\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "@AGENTS.md\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nI am always thrilled to receive pull requests, and I do my best to process them as fast as possible. Not sure if that typo is worth a pull request? Do it! I appreciate it.\n\nIf your pull request is not accepted on the first try, don't be discouraged. We can work together to improve the PR so it can be accepted.\n\n## Legal\n\nAll original contributions are licensed under the ASL - Apache License, version 2.0 or later, or, if another license is specified as governing the file or directory being modified, such other license.\n\nAll contributions are subject to the [Developer Certificate of Origin (DCO)](https://developercertificate.org/). The DCO text is also included verbatim in [dco.txt](dco.txt).\n\n## Please open an issue first\n\nTake a moment to check that an issue doesn't already exist for the behavior or output you're seeing.\nIf it does, please add a quick thumbs up reaction.\nThis will help prioritize the most common problems and requests.\n\nIf there isn't an issue yet, open one! Be as specific as you can, and include the tool version.\n\n## Build and test the CLI\n\n[Install Java](https://adoptium.net/installation/). This project requires Java 17.\n\n- **Use maven:** `./mvnw install`\n- **Use the Quarkus CLI**: `quarkus build`\n\nTo test with actual/live data, add 5eTools and/or PF2eTools into a `sources` directory.\n\nUsing the GitHub CLI:\n\n```shell\nmkdir -p sources\n\n# PF2eTools\ngh repo clone Pf2eToolsOrg/Pf2eTools sources/Pf2eTools -- --depth=1\n```\n\n> [!NOTE]\n> Java 17 and Java 21 will work without issue.\n> There are known issues with picocli and Java 24; avoid it.\n\n### Building native images\n\nA Pull Request build will automatically build a native image for Linux. Building a native image requires a few more steps (GraalVM, native-image, etc). See the [Quarkus documentation](https://quarkus.io/guides/building-native-image) for more information.\n\n### Build CSS only\n\n- **build css**: `./mvnw sass-cli:run`\n- **watch**: `./mvnw sass-cli:watch`\n- **package**: `./mvnw sass-cli:run -Dsass.watch`\n\n## Running tests in the IDE\n\nI'll just talk about VS Code here. IntelliJ has similar features.\n\nTo run tests, you need to have live data in the `sources` directory as described above.\n\n1. Install and enable the following extensions:\n    - Language support for Java\n    - Debugger for Java\n    - Test Runner for Java\n\n    Installing these projects will add a few icons to the activity bar on the left:\n        - A beaker: that's the Test Explorer view.\n        - A play button with a bug: that's the Run/Debug view.\n\n2. Open the Test Explorer view. You will see a hierarchical view of all the tests in the project.\n\n    - You can run individual tests, or run all the tests in a package or class.\n    - Tests of note:\n        - `dev.ebullient.convert.Tools5eDataConvertTest`: this tests runs against a launch of the CLI. There are different methods here for different input parameters (SRD-only, constrained input, all input, collection of homebrew).\n        - `dev.ebullient.convert.tools.dnd5e.*`: This is a collection of tests (`JsonDataNoneTest` is SRD-only, `JsonDataSubsetTest` uses a subset of the data, `JsonDataTest` uses all the data) that all share a common set of test cases defined here: `src/test/java/dev/ebullient/convert/tools/dnd5e/CommonDataTests.java`. There is a method for each type of data, which makes it a bit easier to debug how a certain type is rendered.\n\n    Notes:\n        - You will never run \\*IT tests from the Test Explorer. These are run by the maven build against the constructed final artifact.\n\n## Coding standards and Conventions\n\nThere are two tasks in the Maven build that will format code and sort imports to reduce whitespace/ordering churn in PRs and commits.\nMake sure to run `./mvnw package` or `./mvnw process-sources` before you commit, and everything will be formatted correctly.\n\nThis project also has a `.editorconfig` file that defines expected behavior for whitespace and line endings.\nMost IDEs have an editorconfig plugin that will automatically format your code to match these settings.\n\n### IDE Config and Code Style\n\nIf you want your IDE to format your code for you, use the files in the `src/ide-config` directory.\n\n#### Eclipse Setup\n\nOpen the *Preferences* window, and then navigate to *Java* -> *Code Style* -> *Formatter*.\n\n- Click *Import* and then select `src/ide-config/eclipse-format.xml`.\n- Choose the `ttrpg-convert-cli` profile.\n\nNext navigate to *Java* -> *Code Style* -> *Organize Imports*. Click *Import* and select `src/ide-config/eclipse.importorder`.\n\n#### VS Code\n\n1. Install and enable the \"Language Support for Java by Red Hat\" extension.\n2. Open settings (use the gear icon in the lower left corner of the window), and click the \"Workspace\" link in the header under the search box to open the workspace settings.\n3. Type `eclipse` in the search box, you should see two: `Java > Format: Eclipse` and `Java > Format: Eclipse Export Profile`.\n4. Set *Java* -> *Format* -> *Settings URL* to `https://raw.githubusercontent.com/ebullient/ttrpg-convert-cli/main/src/ide-config/eclipse-format.xml`\n5. Set *Java* -> *Format* -> *Profile* to `ttrpg-convert-cli`\n6. Click \"Edit in settings.json\" under *Java* -> *Completion* -> *Import Order* and paste the following into the `java.format.imports.order` setting:\n\n    ```json\n    \"java.completion.importOrder\": [\n      \"#\",\n      \"java\",\n      \"javax\",\n      \"jakarta\",\n      \"org\",\n      \"com\",\n      \"\"\n    ],\n    ```\n\n#### IntelliJ\n\n1. Open the *Settings* window\n2. Navigate to *Editor* -> *Code Style* -> *Java*\n3. Click on the little cog near the top of the dialogue box, next to the *Scheme* text box. Click on *Import Scheme* -> *Eclipse XML Profile* from the dropdown.\n4. Navigate to `src/ide-config/eclipse-format.xml` in the project directory and click *Ok*, and *Ok* again on the next dialogue box\n5. Click on the *Imports* tab\n6. Ensure that the *Layout static imports separately* checkbox is checked\n7. Underneath that, change the entries so that they look like this:\n\n    ```shell\n   import static all other imports\n   <blank line>\n   import java.*\n   import javax.*\n   import jakarta.*\n   import org.*\n   import com.*\n   <blank line>\n   import all other imports\n    ```\n\n## Notes on control flow\n\n- `src/main/java/dev/ebullient/convert/RpgDataConvertCli.java` is the entry point for the CLI.\n- `src/main/java/dev/ebullient/convert/config` has classes for handling config\n- `src/main/java/dev/ebullient/convert/io` has classes for handling input and output.\n    - `MarkdownDoclet` renders Javadoc in \\*.qute.\\* packages.\n    - `MarkdownWriter` writes markdown files using configured templates.\n    - `Tui` is the \"text user interface\" and deals with writing things to the console.\n- `src/main/java/dev/ebullient/convert/qute` has base/common model/POJO[^1] classes for rendering templates\n- `src/main/java/dev/ebullient/convert/tools` has the main logic for converting data\n    - There are some common interfaces and classes for handling data, and then there are subpackages for each tool.\n\n[^1]: Plain Old Java Object\n\n### Unconventional conventions\n\nThis project is parsing JSON built for JavaScript that has a lot of Union types and other things that are not easily represented in Java. Some of the typical kinds of adapters are not used here: it's too much magic that becomes too hard to read.\n\nThis project does use Jackson, but sticks with raw types (`JsonNode`, `ArrayNode`, `ObjectNode`) to a large extent. There are some cases where direct mapping to an object is used, but it's not the norm.\n\nAs happens with many projects, I've somewhat changed my approach to things over time. When I first started, it was a lot of strings, which lead to a lot of bugs. When working with Pf2e, I stumbled on an approach that is cleaner, but definitely non-standard.\n\nFlow goes something like this:\n\n- `Index`: CLI is launched. Index is created (`Tools5eIndex` or `Pf2eIndex`). Files are read, with json data parsed and indexed as `JsonNode` data.\n- `Prepare`: After all data is read, it is \"prepared\" (for example, fix missing linkages between types after everything has been indexed)\n- `Render`: Selected types are then converted to Markdown using `Json2\\*` classes. These classes parse the type-specific data, and construct a `Qute` model used when rendering templates.\n\nParsing is based on a hierarchy of interfaces with default methods\n\n- 5e: `JsonTextConverter` <- `JsonTextReplacement` <- `JsonSource` <- `Json2QuteCommon` <- `Json2QuteBackground`\n- Pf2e: `JsonTextConverter` <- `JsonTextReplacement` <- `JsonSource`<- `Pf2eTypeReader` <- `Json2QuteBase` <- `Json2QuteBackground`\n\nFields in these Json objects are also read through an enum type hierarchy. These enums allow fields to be defined and typed once (no finger checks) without requiring strict object typing (given union types). This is the preferred practice (for this project), but is not uniformly followed on the (older) 5e side. As I go through things, I'm trying to clean up the 5e side to match the Pf2e side. It's a bit of a mess, but it's a mess that's getting better.\n\n## Commits and pull requests\n\n- Rebase your commits (no merge commits)\n- Use one or more [gitmoji](https://gitmoji.dev/) (the actual emoji, not text) at the beginning of your commits.\n    There are gitmoji plugins/extensions for vscode and intellij, and those help with \"pick the right emoji for this commit\" prompting.\n\n    ```txt\n    🔥🤯 Support 2024 rule changes💥\n    🤖 update generated content\n    🚚 Rename pf2e/JsonSourceCopier to Pf2eJsonSourceCopier\n    🐛✨💥 Make spell durations and cast durations rich data objects\n    ```\n\n    - I'm strict/careful about bugs (🐛), new/shiny things (✨), refactoring (♻️), and CI/build things (👷).\n    - Do not use lipstick; use 🎨 instead.\n    - Use sparkles (✨) if you are adding something new that should be noted in the [CHANGELOG](./CHANGELOG.md)\n    - Use something fiery (🔥💥) if the commit includes breaking changes that should be noted in the [CHANGELOG](./CHANGELOG.md)\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README-WINDOWS.md",
    "content": "# Running on Windows\n\n> [!TIP]\n> See also Obsidian TTRPG Tutorials: [TTRPG-Convert-CLI 5e][] or [TTRPG-Convert-CLI PF2e][]\n\n[TTRPG-Convert-CLI 5e]: https://obsidianttrpgtutorials.com/Obsidian+TTRPG+Tutorials/Plugin+Tutorials/TTRPG-Convert-CLI/TTRPG-Convert-CLI+5e\n[TTRPG-Convert-CLI PF2e]: https://obsidianttrpgtutorials.com/Obsidian+TTRPG+Tutorials/Plugin+Tutorials/TTRPG-Convert-CLI/TTRPG-Convert-CLI+PF2e\n\n## Requirements\n\n- [Git][] is recommended to easily download and update the JSON sources\n- Existing experience with using a command line isn't required, but may be useful. These instructions should be\n  sufficient, but you can look at the following resources for additional background on how to use the Windows\n  command line:\n    - [A Beginner's Guide to the Windows Command Line](https://www.makeuseof.com/tag/a-beginners-guide-to-the-windows-command-line/)\n    - [How to Open Command Prompt in a Folder](https://www.lifewire.com/open-command-prompt-in-a-folder-5185505)\n\n## Instructions\n\n> [!TIP]\n> There are actually three(ish) different kinds of command line programs on Windows. Command Prompt, PowerShell\n> and Terminal. For our purposes they're mostly interchangeable, but the default program is different\n> depending on what version of Windows you have, so if you see e.g. \"PowerShell\" mentioned, but you have\n> \"Command Prompt\", don't worry - they should all work the same for the instructions here.\n\n1. From the [latest release][1], download the following files:\n\n    - `ttrpg-convert-cli-3.3.1-windows-x86_64.zip`\n    - `ttrpg-convert-cli-3.3.1-examples.zip`\n\n2. Unzip the downloaded files into a place you'll remember. For example, `Downloads`.\n3. Navigate to the `bin` directory inside the unzipped files. It might be nested within another folder. You should see a `ttrpg-convert` EXE file in the folder - see the screenshot below.\n4. In Explorer, hold **Shift** and **Right Click** within the folder (not on any particular file). Select\n   *Open in Terminal* (this may also be *Open PowerShell window here*, or *Open command window here* if you\n   have an older version of Windows)\n\n   ![A screenshot of Windows Explorer showing a \"ttrpg-convert.exe\" file There is a right-click context menu with the \"Open PowerShell window here\" option highlighted](docs/screenshots/windows-explorer-folder-context-menu.png)\n5. A new window should open up, showing something like this. The path to the left of the cursor should match\n   wherever you extracted the files to:\n\n   ![A screenshot of a Windows Powershell window opened to the ttrpg-convert-cli directory](docs/screenshots/windows-powershell-open.png)\n\n6. Run the tool to check that it works. Enter `./ttrpg-convert --version`  following into the terminal and press Enter\n   to run the command. You should see something like the following:\n\n    ```shell\n    PS C:\\Users\\Kelly\\Downloads\\ttrpg-convert-cli-2.3.14-windows-x86_64\\bin> .\\ttrpg-convert --version\n    ttrpg-convert version 2.3.14\n    Git commit: 6ecb310\n    ```\n\n   If this works, then you're good to run the command to generate your notes. Otherwise, look below\n   for troubleshooting instructions\n\n7. Run the tool to generate your notes. What this looks like depends on what you want the tool to do\n   and is described more in detail elsewhere in the README. For example, to generate notes from the\n   D&D5e SRD into a folder called `dm`, run:\n\n    ```shell\n    ./ttrpg-convert --index -o dm\n    ```\n\n    - You should see output like the following, listing out how many notes of each type were generated, and a new `dm` folder should be in that directory.\n\n    ![A screenshot of a Windows Explorer window with a ttrpg-convert.exe file next to a 5etools-mirror-2.github.io folder and a dm folder, and a Powershell window showing the output of the ttrpg-convert command](docs/screenshots/windows-explorer-powershell-after-run.png)\n\n8. To use additional sources, templates, or books, or for more configuration options,\n   [create a config file][3] and [see the main README][4].\n\n    - For example, assuming you have a custom configuration located in a file called `dm-sources.json`, you can use this command to generate notes using that configuration:\n\n    ```shell\n    ./ttrpg-convert --index -o dm -c dm-sources.json\n    ```\n\n[1]: https://github.com/ebullient/ttrpg-convert-cli/releases/latest\n[3]: docs/configuration.md\n[4]: README.md\n\n## Uh oh, something went wrong\n\n### What are the weird characters in the output?\n\nOn Windows, the command output will look like this, with weird characters at the start of lines.\n\n```shell\n[ Γ£à  OK] Finished reading config.\nΓÅ▒∩╕Å Reading C:\\Users\\Kelly\\Downloads\\ttrpg-convert-cli-3.3.1-windows-x86_64\\ttrpg-convert-cli-3.3.1-windows-x86_64\\bin\\5etools-mirror-2.github.io\\data\n[ Γ£à  OK] Finished reading data.\n```\n\nThese are emoji that Windows is having trouble displaying. This doesn't affect the functionality at all, but\nif you want to see these properly, choose a font with emoji support in the command line, and run the following:\n\n```shell\nchcp 65001\n```\n\nYou should then start seeing the emoji correctly:\n\n```shell\n[ ✅  OK] Finished reading config.\n⏱️ Reading C:\\Users\\Kelly\\Downloads\\ttrpg-convert-cli-3.3.1-windows-x86_64\\ttrpg-convert-cli-3.3.1-windows-x86_64\\bin\\5etools-mirror-2.github.io\\data\n[ ✅  OK] Finished reading data.\n```\n\n### 'ttrpg-convert' is not recognized\n\nIf you see the following:\n\n```shell\n'ttrpg-convert' is not recognized as an internal or external command,\noperable program or batch file.\n```\n\nor\n\n```shell\nttrpg-convert : The term 'ttrpg-convert' is not recognized as the name of a cmdlet, function, script file, or operable\nprogram. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.\nAt line:1 char:1\n+ ttrpg-convert\n+ ~~~~~~~~~~~~~\n    + CategoryInfo          : ObjectNotFound: (ttrpg-convert:String) [], CommandNotFoundException\n    + FullyQualifiedErrorId : CommandNotFoundException\n```\n\nThis means that the command line can't find the program. This is usually because you're running the command in\nthe wrong directory, or there's a typo somewhere in the name of the command.\n\nType in `dir` and press **Enter**. You should see output similar to this:\n\n```shell\n    Directory:\n    C:\\Users\\Kelly\\Downloads\\ttrpg-convert-cli-3.3.1-windows-x86_64\\ttrpg-convert-cli-3.3.1-windows-x86_64\\bin\n\n\nMode                 LastWriteTime         Length Name\n----                 -------------         ------ ----\nd-----        29/06/2024   6:51 PM                5etools-mirror-2.github.io\n-a----        29/06/2024   4:17 PM       58880000 ttrpg-convert.exe\n```\n\nIf there is no `ttrpg-convert.exe` in the list, then you're either in the wrong directory or have unzipped the\nfile to the wrong directory. Make sure that you're opening the command line in the directory that contains\n`ttrpg-convert.exe`.\n\nIf there *is* a `ttrpg-convert.exe` in the list, then the next most likely culprit is a typo. Make sure that the\ncommand starts with `./ttrpg-convert`. Try copy/pasting this command:\n\n```shell\n./ttrpg-convert --help\n```\n\nIf everything is set up correctly, you should see output starting with the following:\n\n```shell\nConvert TTRPG JSON data to markdown\nUsage: ttrpg-convert [-dhlvV] [--index] [-c=<configPath>] [-g=<datasource>]\n                     -o=<outputPath> [] [<input>...] [COMMAND]\n```\n\n### No output at all\n\nIf you don't get any output at all when running the `ttrpg-convert` command, try running\n`./ttrpg-convert --help`. If you still get no output, like this:\n\n```shell\nC:\\Users\\Kelly\\Downloads\\ttrpg-convert-cli-2.3.14-windows-x86_64\\bin>.\\ttrpg-convert --help\n\nC:\\Users\\Kelly\\Downloads\\ttrpg-convert-cli-2.3.14-windows-x86_64\\bin>\n```\n\nThen you probably have an anti-virus software that is blocking command line EXE files. The standard Windows\nDefender doesn't do this, so it's probably some third-party anti-virus software you have installed. Try\ndisabling it temporarily, or allow-listing the `ttrpg-convert.exe` file.\n\nNote that this is often different from the Firewall settings and is often listed as a different feature with\nwith a different name, depending on what anti-virus software you're using. Sometimes this is called\n*Realtime Protection*, or *Deep Behavioral Inspection*.\n\n### The current machine does not support all of the following CPU features that are required by the image\n\nIf you see the following:\n\n> The current machine does not support all of the following CPU features that are required by the image:\n> \\[CX8, CMOV, FXSR, MMX, SSE, SSE2, SSE3, SSSE3, SSE4_1, SSE4_2, POPCNT, LZCNT, AVX, AVX2, BMI1, BMI2, FMA].\n> Please rebuild the executable with an appropriate setting of the -march option.\n\nYou have an older version of Windows. You'll need to use the [Java version](docs/alternateRun.md#use-java-to-run-the-jar) of the CLI instead.\n\n[Git]: https://git-scm.com/download/win\n"
  },
  {
    "path": "README.md",
    "content": "# TTRPG convert CLI\n\n![GitHub all releases](https://img.shields.io/github/downloads/ebullient/ttrpg-convert-cli/total?color=success)\n\nA Command-Line Interface designed to convert TTRPG data from 5eTools and Pf2eTools into crosslinked, tagged, and formatted markdown optimized for [Obsidian.md](https://obsidian.md).\n\n<!-- markdownlint-disable no-inline-html -->\n<table><tr>\n<td>Jump</td>\n<td><a href=\"#install-the-ttrpg-convert-cli\">⬇ Download</a></td>\n<td><a href=\"docs/configuration.md\">⚙️ Configuration</a></td>\n<td><a href=\"examples/\">🎨 Examples</a></td>\n<td><a href=\"examples/templates\">🎨 Templates</a></td>\n</tr><tr>\n<td><a href=\"CHANGELOG.md\">🚜 Changelog</a></td>\n<td><a href=\"docs/sourceMap.md\">🗺️ Source Map</a></td>\n<td><a href=\"#convert-5etools-json-data\">📖 5eTools</a></td>\n<td><a href=\"#convert-pf2etools-json-data\">📖 Pf2eTools</a></td>\n<td><a href=\"#convert-homebrew-json-data\">📖 Homebrew</a></td>\n</tr></table>\n\nI use [Obsidian](https://obsidian.md) to keep track of my campaign notes. This project parses JSON sources for materials that I own from the 5etools mirror to create linked and formatted markdown that I can reference in my notes.\n\n> [!TIP]\n>\n> - 🚜 [**Review the changelog**](CHANGELOG.md) for new capabilities (✨) and breaking changes (🔥💥).\n> - 🔮 Check out [**Conventions**](#conventions) and  [**Recommendations**](#recommendations-for-using-the-cli).\n\n## Using the Command Line\n\nThis tool works in the command line, which is a text-based way to give instructions to your computer.\nIf you're new to it, we have resources to help you get started below.\n\nIf you don't have a favorite method already, or you don't know what those words mean, here are some resources to get you started:\n\n- For macOS / OSX users:\n    - Start with the built-in `Terminal` application.\n    - [Learn the macOS Command Line][]\n- For Windows users:\n    - [A Beginner's Guide to the Windows Command Line][]\n    - See the [Windows README](README-WINDOWS.md)\n\n[Learn the macOS Command Line]: https://blog.teamtreehouse.com/introduction-to-the-mac-os-x-command-line\n[A Beginner's Guide to the Windows Command Line]: https://www.makeuseof.com/tag/a-beginners-guide-to-the-windows-command-line/\n\n## Install the TTRPG Convert CLI\n\nThere are several options for running `ttrpg-convert`.\nChoose the one you are most comfortable with:\n\n- **Using Windows?** See the [Windows README](README-WINDOWS.md).\n- `jbang`: [Use JBang!][jbang] (hides Java; sets up command aliases).\n- `brew`: [Use Homebrew (macOS or Linux)][brew] (uses platform binaries).\n- `bin`: [Use a pre-built platform binary][bin] (no Java required).\n- `jar`: [Use Java to run the jar][jar].\n- `src`: [Build from source][src].\n\n| Platform       | Options  |\n|----------------|----------|\n|  Linux         | [jbang][], [brew][], [bin][], [jar][], [src][] |\n|  Mac (Arm)     | [brew][], [jbang][], [bin][], [jar][], [src][] |\n|  Mac (Intel)   | [brew][], [jbang][], [bin][], [jar][], [src][] |\n|  Windows       | [📝](README-WINDOWS.md), [jbang][], [bin][], [jar][], [src][]  |\n|  Windows (Old) | [📝](README-WINDOWS.md), [jbang][], [jar][], [src][]  |\n|  Windows (WSL) | [brew][], [jbang][], [jar][], [src][] |\n\n[jbang]: ./docs/alternateRun.md#use-jbang\n[brew]: ./docs/alternateRun.md#use-homebrew\n[bin]: ./docs/alternateRun.md#use-pre-built-platform-binary\n[jar]: ./docs/alternateRun.md#use-java-to-run-the-jar\n[src]: ./docs/alternateRun.md#build-and-run-from-source\n\n## Recommendations for using the CLI\n\n- 🔐 Treat generated content as a big ball of mud. Stick it in a corner of your vault and *treat it as read-only*.\n\n    Trust us, you will want to regenerate content from time to time. It is cheap and easy to do if you don't have your own edits to worry about.\n\n- 🔎 Have the CLI generate output into a separate directory and use a comparison tool to preview changes.\n\n    You can use `git diff` to compare arbitrary directories. For example:\n\n    ```bash\n    git diff --no-index vault/compendium/bestiary generated/compendium/bestiary\n    ```\n\n- 📑 Use a copy tool that only updates modified files, like [rsync][], to avoid unnecessary file copying when updating your vault. Use the checksum option (`-c`) to compare file contents; the file modification date is meaningless given generated files are recreated when the tool is run. We have some suggestions in [discussion #220][sync-discussion], but it is very much a work in progress.\n\n[rsync]: https://stackoverflow.com/a/19540611\n[sync-discussion]: https://github.com/ebullient/ttrpg-convert-cli/discussions/220\n\n### Required Plugins\n\n- **Admonitions** ([git](https://github.com/javalent/admonitions)/[obsidian](obsidian://show-plugin?id=obsidian-admonition)): The Admonitions plugin supports a codeblock style admonition used for more complex embedded content. See [Admonition plugin notes](docs/README.md#admonitions) for more recommendations.\n\n### Recommended Plugins\n\n- **Force Note View Mode by Front Matter** ([git](https://github.com/bwydoogh/obsidian-force-view-mode-of-note)/[obsidian](obsidian://show-plugin?id=obsidian-view-mode-by-frontmatter)): I use this plugin to treat these generated notes as essentially read-only. See [Force Note View Mode plugin settings](docs/README.md#force-note-view-mode-by-front-matter) for recommendations.\n\n- **Fantasy Statblocks** ([git](https://github.com/javalent/fantasy-statblocks)/[obsidian](obsidian://show-plugin?id=obsidian-5e-statblocks)): Templates for rendering monsters can define a `statblock` in the document body or provide a full or abridged YAML monster in the document header to update monsters in the plugin's bestiary.\n    - See [Fantasy Statblocks plugin settings](docs/README.md#fantasy-statblocks) for recommendations.\n    - See [Templates](examples/templates) for related template customization.\n\n- **Initiative Tracker** ([git](https://github.com/javalent/initiative-tracker)/[obsidian](obsidian://show-plugin?id=initiative-tracker)): Templates for rendering monsters can include information in the header to define monsters that the Initiative Tracker can use when constructing encounters. See [Initiative Tracker plugin settings](docs/README.md#initiative-tracker) for recommendations.\n\n## Conventions\n\n- **Links.** Documents generated by this plugin will use markdown links rather than wiki links. A [CSS snippet](examples/css-snippets/hide-markdown-link-url.css) can make these links less intrusive in edit mode by hiding the URL portion of the string.\n\n- **File names.** To avoid conflicts and issues with different operating systems, all file names are slugified (all lowercase, symbols stripped, and spaces replaced by dashes). This is a familiar convention for those used to Jekyll, Hugo, or other blogging systems.\n\n    - File names for resources outside of the core books (PHB, MM, and DMG) have the abbreviated source name appended to the end to avoid file name collisions.\n    - All files have an `aliases` attribute that contains the original name of the resource.\n\n- **Organization.** Files are generated in two roots: `compendium` and `rules`. The location of these roots is [configurable](docs/configuration.md#specify-target-paths-paths-key). These directories will be populated based on the sources you have enabled.\n\n    - `compendium` contains files for items, spells, monsters, etc. The `compendium` directory is further organized into subdirectories for each type of content. For example, all items are in the `compendium/items` directory.\n\n    - `rules` contains files for conditions, weapon properties, variant rules, etc.\n\n    - `css-snippets` will contain **CSS files for special fonts** used by some content. You will need to copy these snippets into your vault (`.obsidian/snippets`) and enable them (`Appearance -> Snippets`) to ensure all content in your vault is styled correctly.\n\n- **Styles.** Every document has a `cssclasses` attribute that assigns a CSS class. We have some [CSS snippets](examples/css-snippets/) that you can use to customize elements of the compendium.\n    - 5eTools: `json5e-background`, `json5e-class`, `json5e-deck`, `json5e-deity`, `json5e-feat`, `json5e-hazard`, `json5e-item`, `json5e-monster`, `json5e-note`, `json5e-object`, `json5e-psionic`, `json5e-race`, `json5e-reward`, `json5e-species`, `json5e-spell`, and `json5e-vehicle`.\n    - Pf2eTools: `pf2e`, `pf2e-ability`, `pf2e-action`, `pf2e-affliction`, `pf2e-archetype`, `pf2e-background`, `pf2e-book`, `pf2e-deity`, `pf2e-feat`, `pf2e-hazard`, `pf2e-index`, `pf2e-item`, `pf2e-note`, `pf2e-ritual`, `pf2e-spell`, `pf2e-trait`.\n\n- **Admonitions.** Generated content uses code-block-style [Admonitions](docs/README.md#admonitions) in addition to Obsidian callouts. We have [Admonition definitions](examples/admonitions/) that you can import to ensure these admonition/callout types are defined.\n    - `ad-statblock`\n    - 5eTools: `ad-flowchart`, `ad-gallery`, `ad-embed-action`, `ad-embed-feat`, `ad-embed-monster`, `ad-embed-object`, `ad-embed-race`, `ad-embed-spell`, `ad-embed-table`\n    - Pf2eTools: `ad-embed-ability`, `ad-embed-action`, `ad-embed-affliction`, `ad-embed-avatar`, `ad-embed-disease`, `ad-embed-feat`, `ad-embed-item`, `ad-pf2-note`, `ad-pf2-ritual`.\n\n## Convert 5eTools JSON data\n\n> [!NOTE]\n> Instructions here use backslashes to wrap lines for readability (a common practice for Linux-based command shells).\n>\n> *If you are using Windows*, you will need to remove the backslashes and put the command on a single line. You may also need to append `.exe` to the command name (though it should work without).\n\n1. Invoke the CLI with the `--version` option.\n\n    ```shell\n    ttrpg-convert --version\n    ```\n\n    You should see output similar to the following:\n\n    ```shell\n    ttrpg-convert version 2.3.18\n    Git commit: ed56f76\n    ```\n\n    If you do, we know that the CLI is working!\n\n    If you don't, there may be something wrong with your installation. Windows users, see the [Windows README for help](./README-WINDOWS.md#uh-oh-something-went-wrong).\n\n2. Invoke the CLI to generate indexes and markdown for SRD content:\n\n    ```shell\n    ttrpg-convert \\\n      --index \\\n      -o dm \\\n      <5etools-data-dir>\n    ```\n\n    - `--index` generates two index files: `all-index.json` and `src-index.json`.\n\n        > 🚀 TIP:\n        > - Use `all-index.json` to see the reference keys for all discovered content. This can confirm that an included source was actually read.\n        > - Use `src-index.json` to see the reference keys for content that was included in the generated output. Use this to confirm that your source selection is working as expected.\n\n    - `-o dm` The target output directory (`dm` in this case). Files will be created in this directory.\n\n    - `<5etools-data-dir>` is a placeholder for the location of downloaded 5eTools source data directory.\n\n    This should produce a set of markdown files in the `dm` directory that contains only SRD content.\n\n3. Next, you'll want to create a [configuration file](docs/configuration.md) to set up your sources.\n\n    The configuration is provided to the CLI using the `-c` option:\n\n    ```shell\n    ttrpg-convert \\\n        --index \\\n        -o dm \\\n        -c my-config.json \\\n        <5etools-data-dir>\n    ```\n\n    > 🚀 Note: Only include content you own. Find the identifier for your sources in the [Source Map](./docs/sourceMap.md#source-name-mapping-for-5etools).\n\nNext step:\n\n- Create your own [configuration file](docs/configuration.md).\n\n## Convert Pf2eTools JSON data\n\n🚜 🚧 🚜 🚧 🚜 🚧 🚜 🚧\n\n> [!NOTE]\n> Instructions here use backslashes to wrap lines for readability (a common practice for Linux-based command shells).\n>\n> *If you are using Windows*, you will need to remove the backslashes and put the command on a single line. You may also need to append `.exe` to the command name (though it should work without).\n\n1. Invoke the CLI with the `--version` option:\n\n    ```shell\n    ttrpg-convert --version\n    ```\n\n    You should see output similar to the following:\n\n    ```shell\n    ttrpg-convert version 2.3.18\n    Git commit: ed56f76\n    ```\n\n    If you do, we know that the CLI is working!\n\n    If you don't, there may be something wrong with your installation. Windows users, see the [Windows README for help](./README-WINDOWS.md#uh-oh-something-went-wrong).\n\n2. Invoke the CLI. In this first example, let's generate indexes and markdown for default content:\n\n    ```shell\n    ttrpg-convert \\\n      -g pf2e \\\n      --index \\\n      -o dm \\\n      <Pf2eTools-data-dir>\n    ```\n\n    - `-g pf2e` The game system! Pathfinder 2e!\n    - `--index` generates two index files: `all-index.json` and `src-index.json`.\n\n      > 🚀 TIP:\n      > - Use `all-index.json` to see the reference keys for all discovered content. This can confirm that an included source was actually read.\n      > - Use `src-index.json` to see the reference keys for content that was included in the generated output. Use this to confirm that your source selection is working as expected.\n\n    - `-o dm` The target output directory. Files will be created in this directory.\n\n    - `<Pf2eTools-data-dir>` is a placeholder for the location of downloaded 5eTools source data directory.\n\n3. Next, you'll want to create a [configuration file](docs/configuration.md) to set up your sources.\n\n    The configuration is provided to the CLI using the `-c` option:\n\n    ```shell\n    ttrpg-convert \\\n        -g pf2e \\\n        --index \\\n        -o dm \\\n        -c my-config.json\n        <Pf2eTools-data-dir>\n    ```\n\n    > 🚀 Note: Only include content you own. Find the identifier for your sources in the [Source Map](./docs/sourceMap.md#source-name-mapping-for-pf2etools).\n\nNext step:\n\n- Create your own [configuration file](docs/configuration.md).\n\n## Convert Homebrew JSON data\n\nThe CLI tool can also import homebrewed content, though this content must still fit the JSON standards set by the [5eTools JSON spec][5etools JSON] or the PF2eTools JSON spec (coming soon, similar to 5eTools).\n\nPerhaps the simplest way to import homebrew is to use existing homebrew data from the 5eTools homebrew GitHub repo: <https://github.com/TheGiddyLimit/homebrew>.\n\n> [!TIP]\n> 🍺 *You only need the specific file you wish to import*.\n>\n> Homebrew data is different from the 5eTools data. Each homebrew file is a complete reference. If you compare it to cooking: the 5eTools mirror repo is organized by ingredient (all of the carrots, all of the onions, ...); homebrew data is organized by prepared meal / complete recipe.\n\nAdding homebrew content is easiest if you use a [configuration file](./docs/configuration.md). We will assume a file named `my-config.json` for the example below, but you can use any name you like.\n\n> [!IMPORTANT]\n> 🚀 Respect copyrights and support content creators; use only the sources you own.\n\nFor example, if you want to use Benjamin Huffman's popular homebrewed [Pugilist class](https://www.dmsguild.com/product/184921/The-Pugilist-Class):\n\n1. Download a copy of the [Pugilist JSON file](https://github.com/TheGiddyLimit/homebrew/blob/master/class/Benjamin%20Huffman%3B%20Pugilist.json).\n\n    Save this file to a well-known location on your computer. It is probably easiest if it sits next to your 5eTools or Pf2eTools directory.\n\n2. Update your [configuration file](docs/configuration.md) to add a `homebrew` section under `sources`:\n\n    ```json\n    {\n        \"sources\": {\n            ...\n            \"homebrew\": [\n                \"path/to/Benjamin Huffman; Pugilist.json\"\n            ]\n        }\n    }\n    ```\n\n    - `path/to/` is a placeholder for a relative or absolute path to the file[^1]. Here are a few ways to determine the path to a file:\n        - You may be able to drag and drop the file into the terminal window.\n        - You may be able to right-click on the file and select \"Copy Path\".\n        - *Windows users*: When pasting the path into a text editor, use find/replace to replace all `\\` with `/`.\n\n3. Run the command like so (for 5e homebrew):\n\n    ```shell\n    ttrpg-convert \\\n        --index \\\n        -o hb-compendium \\\n        -c my-config.json \\\n        <5etools-data-dir>\n    ```\n\n    - `-o hb-compendium` specifies the output directory for generated content.\n    - `-c my-config.json` specifies the name and/or path to your configuration file.\n    - `<5etools-data-dir>` is a placeholder for the 5eTools source data directory.\n\nSee [configuration](docs/configuration.md) for more details on how to configure the CLI.\n\nThe process is similar for other homebrew, including your own, as long as it is broadly compatible with the [5eTools JSON spec](https://wiki.tercept.net/en/Homebrew/FromZeroToHero).\n\n## Where to find help\n\n- There is a `#cli-support` thread in the `#tabletop-games` channel of the [Obsidian Discord](https://discord.gg/veuWUTm).\n- There is a `TTRPG-convert-help` post in the `obsidian-support` forum of the [Obsidian TTRPG Community Discord](https://discord.gg/Zpmr37Uv).\n- There is a TTRPG-convert tutorial (currently aimed at Windows users, but much of it is helpful no matter your OS) at [Obsidian TTRPG Tutorials](https://obsidianttrpgtutorials.com/Obsidian+TTRPG+Tutorials/Plugin+Tutorials/TTRPG-Convert-CLI/TTRPG-Convert-CLI+5e).\n- If you open an issue for an error, run with the `--debug` and `--log` options, and attach the log file to the issue.\n\n### Want to help fix it?\n\n- If you're familiar with the command line and are comfortable running the tool, please consider running [unreleased snapshots](docs/alternateRun.md#using-unreleased-snapshots) and reporting issues.\n- If you want to contribute, I'll take help of all kinds: documentation, examples, sample templates, and stylesheets are just as important as Java code. See [CONTRIBUTING](CONTRIBUTING.md).\n\n## Other notes\n\nThis project uses Quarkus, the Supersonic Subatomic Java Framework. To learn more about Quarkus, please visit its website: <https://quarkus.io/>.\n\nThis project is a derivative of [fc5-convert-cli](https://github.com/ebullient/fc5-convert-cli), which focused on working with FightClub5 Compendium XML files. It has also borrowed some bits and pieces from [pockets-cli](https://github.com/ebullient/pockets-cli).\n\n[5eTools JSON]: https://wiki.tercept.net/en/Homebrew/FromZeroToHero\n\n<a href=\"https://www.buymeacoffee.com/ebullient\" target=\"_blank\"><img src=\"https://cdn.buymeacoffee.com/buttons/v2/default-blue.png\" alt=\"Buy Me A Coffee\" style=\"height: 60px !important;width: 217px !important;\"></a>\n\n[^1]: Description of relative vs absolute file paths: <https://stackoverflow.com/a/10288252>. If you use a relative path, it will be resolved relative to the current working directory, as described here: <https://en.wikipedia.org/wiki/Working_directory>.\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\nI only support the latest version of the CLI. Given changes to source data, running/maintaining older versions isn't feasible.\n\n## Reporting a Vulnerability\n\nPlease report suspected security issues privately using GitHub’s “Report a vulnerability” link in the repository sidebar.\n\nDo **not** open a public issue for suspected vulnerabilities.\n\nWhen reporting, please include:\n\n- A description of the issue and affected versions\n- Steps to reproduce (ideally a minimal proof-of-concept)\n- Assessment of potential impact\n- Your contact information\n- Any specific requests, such as anonymity for you and/or the organization you represent\n"
  },
  {
    "path": "dco.txt",
    "content": "Developer Certificate of Origin\nVersion 1.1\n\nCopyright (C) 2004, 2006 The Linux Foundation and its contributors.\n1 Letterman Drive\nSuite D4700\nSan Francisco, CA, 94129\n\nEveryone is permitted to copy and distribute verbatim copies of this\nlicense document, but changing it is not allowed.\n\n\nDeveloper's Certificate of Origin 1.1\n\nBy making a contribution to this project, I certify that:\n\n(a) The contribution was created in whole or in part by me and I\n    have the right to submit it under the open source license\n    indicated in the file; or\n\n(b) The contribution is based upon previous work that, to the best\n    of my knowledge, is covered under an appropriate open source\n    license and I have the right under that license to submit that\n    work with modifications, whether created in whole or in part\n    by me, under the same open source license (unless I am\n    permitted to submit under a different license), as indicated\n    in the file; or\n\n(c) The contribution was provided directly to me by some other\n    person who certified (a), (b) or (c) and I have not modified\n    it.\n\n(d) I understand and agree that this project and the contribution\n    are public and that a record of the contribution (including all\n    personal information I submit with it, including my sign-off) is\n    maintained indefinitely and may be redistributed consistent with\n    this project or the open source license(s) involved.\n"
  },
  {
    "path": "docs/README.md",
    "content": "# Documentation\n\n- [Other ways to run the CLI](./alternateRun.md)\n- [Create a config file](./configuration.md)\n    - [Configuration Examples][ex-config]\n- [Template Reference][templates]\n    - [Default templates][def-templates]\n    - [Example templates][ex-templates]\n- [Source Map][]\n\n[ex-ad]: ../examples/admonitions\n[ex-config]: ../examples/config\n[ex-templates]: ../examples/templates\n[def-templates]: ../src/main/resources/templates\n[templates]: ./templates/README.md\n[Source Map]: ./sourceMap.md\n\n## Recommended Obsidian plugin configuration\n\n- [Admonitions](#admonitions)\n- [Force note view mode by front matter](#force-note-view-mode-by-front-matter)\n- [Fantasy Statblocks](#fantasy-statblocks)\n- [Initiative Tracker](#initiative-tracker)\n- [Working with Dataview](#working-with-dataview)\n\n### Admonitions\n\nThe [Admonition plugin](https://github.com/javalent/admonitions) provides additional support for codeblock-style admonitions in addition to callouts.\n\nImport one or more admonition json files in [examples/admonitions][ex-ad] to create the custom admonition types used for converted content:\n\n- [admonitions-5e.json](../examples/admonitions/admonitions-5e.json) for 5e tools\n- [admonitions-pf2e.json](../examples/admonitions/admonitions-pf2e-v3.json) for pf2e tools\n- [other-admonitions.json](../examples/admonitions/other-admonitions.json) if they are interesting\n\n### Force note view mode by front matter\n\nUse this plugin to treat these generated notes as essentially read-only.\n\nEnsure the plugin has the following options enabled (for the most consistent and least invasive experience):\n\n- *\"Ignore force view when not in front matter\"*: the plugin will only change the view mode if `obsidianUIMode` is defined in the front matter.\n- *\"Ignore open files\"*: the plugin won't try to change the view mode if the file is already open.\n\n### Fantasy Statblocks\n\n[Fantasy Statblocks](https://github.com/javalent/fantasy-statblocks) maintains a bestiary of monsters. It has its own (handlebars-based) templates for monster statblocks.\n\nTo populate *Fantasy Statblocks* from your notes, use the following settings:\n\n- General Settings:\n    - *Optional*: Disable the 5e SRD\n- Note Parsing:\n    - Enable Parse Frontmatter for Creatures\n    - *Optional*: Add bestiary folders to constrain where the plugin looks for monsters\n\nYou also need to generate content using [templates][5eTools templates] that will populate the bestiary.\n\n### Initiative Tracker\n\nThe [Initiative Tracker](https://github.com/javalent/initiative-tracker) plugin for Obsidian allows you to keep track of initiative and turn order during combat encounters in tabletop role-playing games.\n\n- Basic Settings\n    - *Optional*: Embed statblock-link content in the creature view.  \n        Enable this if you use markdown statblocks and want to see the statblock content in the creature view.\n        `statblock-link` must be set in the frontmatter.\n- Plugin Integrations\n    - *Optional*: Sync monsters from TTRPG Statblocks  \n        Enable this if you use the Fantasy Statblocks plugin and want to sync monsters from your notes to the Initiative Tracker.\n\n        - See [Fantasy Statblocks](#fantasy-statblocks) section for recommended settings.\n        - Make sure you're using compatible [templates][5eTools templates] for your monsters.\n\n[5eTools templates]: ../examples/templates/tools5e/README.md#5etools-alternate-monster-templates\n\n### Working with Dataview\n\nNotes use lower-kebab-case for filenames. While that works very well for data portability and avoiding\nfilename collisions, it doesn't look very pretty.\n\nDataview queries can create entries in tables that use the file's alias instead of the filename, like this:\n\n```md\n~~~dataview\nTABLE without ID link(file.name, aliases) from \"compendium/classes\"\nFLATTEN aliases\n~~~\n```\n"
  },
  {
    "path": "docs/alternateRun.md",
    "content": "# Other ways to run the CLI\n\n- [Use JBang](#use-jbang)\n- [Use Homebrew](#use-homebrew)\n    - [Use pre-built platform binary](#use-pre-built-platform-binary)\n- [Use Java to run the jar](#use-java-to-run-the-jar)\n- [Build and run from source](#build-and-run-from-source)\n- [Using unreleased snapshots](#using-unreleased-snapshots)\n\n[conventions]: ../README.md#conventions\n[5etools-data]: ../README.md#convert-5etools-json-data\n[pf2e-data]: ../README.md#convert-pf2etools-json-data\n[homebrew]: ../README.md#convert-homebrew-json-data\n[config]: ./configuration.md\n[_unreleased snapshot_]: #using-unreleased-snapshots\n[java_install]: https://adoptium.net/installation/\n\n## Use JBang\n\nJBang is a tool designed to simplify Java application execution. By eliminating the need for traditional build tools and app servers, JBang enables quick and easy running of Java apps, scripts, and more.\n\n1. Install JBang: <https://www.jbang.dev/documentation/jbang/latest/installation.html>\n\n2. Install the pre-built release of ttrpg-convert-cli:\n\n    ```shell\n    jbang app install --name ttrpg-convert --force --fresh https://github.com/ebullient/ttrpg-convert-cli/releases/download/3.3.1/ttrpg-convert-cli-3.3.1-runner.jar\n    ```\n\n    🚧 If you want the latest [_unreleased snapshot_][]:\n\n    ```shell\n    jbang app install --name ttrpg-convert --force --fresh https://github.com/ebullient/ttrpg-convert-cli/releases/download/299-SNAPSHOT/ttrpg-convert-cli-299-SNAPSHOT-runner.jar\n    ```\n\n    > 🔹 Feel free to use an alternate alias by replacing the value specified as the name.\n    > For example, for the snapshot, you can use `--name ttrpg-convert-ss`, allowing you to keep both versions available.\n    > You will need to adjust commands accordingly.\n\n3. Verify the install by running the command:\n\n    ```shell\n    ttrpg-convert --help\n    ```\n\n    Notice there is no leading `./` or `.\\`. JBang installs the command in a location that is on your PATH[^1].\n\nNext steps:\n\n- Understand [CLI plugin and usage conventions][conventions].\n- [Convert 5eTools JSON data][5etools-data]\n- [Convert PF2eTools JSON data][pf2e-data]\n- [Convert Homebrew JSON data][homebrew]\n- Create your own [configuration file][config].\n\n## Use Homebrew\n\nNot to be confused with Homebrew adventures, Homebrew is a package manager for Mac OS (and sometimes linux).\n\n1. Install Homebrew: <https://brew.sh/>\n2. Install the `tap`:\n\n    ```shell\n    brew tap ebullient/tap\n    ```\n\n3. Install the cli:\n\n    ```shell\n    brew install ttrpg-convert-cli\n    ```\n\n4. Verify the install by running the command (from anywhere):\n\n    ```shell\n    ttrpg-convert --help\n    ```\n\n    Notice there is no leading `./` or `.\\`. Homebrew installs the command in a location that is on your PATH[^1].\n\nNext steps:\n\n- Understand [CLI plugin and usage conventions][conventions].\n- [Convert 5eTools JSON data][5etools-data]\n- [Convert PF2eTools JSON data][pf2e-data]\n- [Convert Homebrew JSON data][homebrew]\n- Create your own [configuration file][config].\n\n### Use pre-built platform binary\n\n> [!NOTE]\n> 📝 *Where do these binaries come from?*\n>\n> They are built on GitHub managed CI runners using the workflow defined [here](https://github.com/ebullient/ttrpg-convert-cli/blob/main/.github/workflows/release.yml), which compiles a Quarkus application (Java) into a platform-native binary using [GraalVM](https://www.graalvm.org/). I build and upload the mac arm64 binary myself (not supported by GH CI) using [this script](https://github.com/ebullient/ttrpg-convert-cli/blob/main/.github/augment-release.sh).\n\n[Download the latest release](https://github.com/ebullient/ttrpg-convert-cli/releases/latest) of the zip or tgz for your platform. Extract the archive. A `ttrpg-convert` binary executable will be in the extracted bin directory.\n\nIn a terminal or command shell, navigate to the directory where you extracted the archive and run the command in the `bin` directory:\n\n```shell\n# Linux or MacOS (use the leading ./ because the current directory is not in the PATH[^1])\n./ttrpg-convert --help\n\n# Windows (the .exe extension is optional)\nttrpg-convert.exe --help\n```\n\nNotes:\n\n- Windows users: the `.exe` extension is optional. You can run `ttrpg-convert.exe` or `ttrpg-convert` interchangeably.\n- Folks familar with command line tools can add the `bin` directory to their path to make the command available from anywhere.\n- _MacOS permission checking_ (unverified executable): `xattr -r -d com.apple.quarantine <path/to>/ttrpg-convert`\n\nNext steps:\n\n- Understand [CLI plugin and usage conventions][conventions].\n- [Convert 5eTools JSON data][5etools-data]\n- [Convert PF2eTools JSON data][pf2e-data]\n- [Convert Homebrew JSON data][homebrew]\n- Create your own [configuration file][config].\n\n## Use Java to run the jar\n\nTo run the CLI, you will need to have **Java 17** installed on your system.\n\n1. Ensure you have [**Java 17** installed on your system][java_install] and active in your path.\n\n2. Download the CLI as a jar\n\n    - Latest release: [ttrpg-convert-cli-3.3.1-runner.jar](https://github.com/ebullient/ttrpg-convert-cli/releases/download/3.3.1/ttrpg-convert-cli-3.3.1-runner.jar)\n    - 🚧 [_unreleased snapshot_][]: [ttrpg-convert-cli-399-SNAPSHOT-runner.jar](https://github.com/ebullient/ttrpg-convert-cli/releases/download/399-SNAPSHOT/ttrpg-convert-cli-399-SNAPSHOT-runner.jar)\n\n3. Verify the install by running the command:\n\n    ```shell\n    java -jar ttrpg-convert-cli-3.3.1-runner.jar --help\n    ```\n\n    🚧 If you are using the [_unreleased snapshot_][], use the following command:\n\n    ```shell\n    java -jar ttrpg-convert-cli-299-SNAPSHOT-runner.jar --help\n    ```\n\nTo run commands, replace `ttrpg-convert` with `java -jar ttrpg-convert-cli-...`\n\nNext steps:\n\n- Understand [CLI plugin and usage conventions][conventions].\n- [Convert 5eTools JSON data][5etools-data]\n- [Convert PF2eTools JSON data][pf2e-data]\n- [Convert Homebrew JSON data][homebrew]\n- Create your own [configuration file][config].\n\n> [!NOTE]\n> Java 17 and Java 21 will work without issue.\n> There are known issues with picocli and Java 24; avoid it.\n\n## Build and run from source\n\nThis is a Quarkus project that uses Maven as its build tool.\n\n- You can use the [Quarkus CLI](https://quarkus.io/guides/cli-tooling) to build and run the project\n- You can use Maven to build and run the project via the [maven wrapper](https://maven.apache.org/wrapper/) (the `mvnw` script). The Maven Wrapper is a tool that provides a standardized way to execute Maven builds, ensuring the correct version and configurations are used.\n\n1. Clone this repository\n\n2. Ensure you have [**Java 17** installed on your system][java_install] and active in your path.\n\n3. Build this project:\n    - Build with the Quarkus CLI: `quarkus build`\n    - Build with the Maven wrapper: `./mvnw install`\n\n4. Verify the build: `java -jar target/ttrpg-convert-cli-299-SNAPSHOT-runner.jar --help`\n\nTo run commands, either:\n\n- Replace `ttrpg-convert` with `java -jar target/ttrpg-convert-cli-299-SNAPSHOT-runner.jar`, or\n- Use JBang to create an alias that points to the built jar:\n\n    ```shell\n    jbang app install --name ttrpg-convert --force --fresh ~/.m2/repository/dev/ebullient/ttrpg-convert-cli/299-SNAPSHOT/ttrpg-convert-cli-299-SNAPSHOT-runner.jar\n    ```\n\n    > 🔹 Use an alternate alias by replacing the value specified as the name: `--name ttrpg-convert`, and adjust the commands accordingly.\n\nNext steps:\n\n- Understand [CLI plugin and usage conventions][conventions].\n- [Convert 5eTools JSON data][5etools-data]\n- [Convert PF2eTools JSON data][pf2e-data]\n- [Convert Homebrew JSON data][homebrew]\n- Create your own [configuration file][config].\n\n## Using unreleased snapshots\n\nFolks picking up early snapshots is really helpful for me, but _using an unreleased snapshot may be unstable_.\n\n- [Build from source](#build-and-run-from-source)\n- [Use JBang to install the snapshot](#use-jbang)\n\n- 🚧 Do not run an unstable CLI directly against notes in your Obsidian vault\n- 👷‍♀️ Be prepared to report issues if you find them.\n    - Be as specific as you can about the configuration and sources that are not working.\n    - `ttrpg-convert --version` will tell you the version you are running, including the commit! Please include that information in your report.\n\nI recommend staying with official releases unless you are willing to help me debug issues (and your help is very much appreciated!).\n\n[^1]: A PATH is a list of directories that the operating system searches for executables. When you type a command in a terminal, the system looks in each directory in the path for an executable with the name you typed. If it finds one, it runs it. If it doesn't, it reports an error. See [Wikipedia](https://en.wikipedia.org/wiki/PATH_(variable)) for a rough overview and more links.\n"
  },
  {
    "path": "docs/configuration.md",
    "content": "# CLI Configuration guide\n\n> [!IMPORTANT]\n> 🚀 Respect copyrights and support content creators; use only the sources you own.\n\nThis guide introduces you to configuring data transformations using the Command Line Interface (CLI). Whether you're new to command line tools or an experienced user, you'll find helpful information on utilizing configuration files to tailor your experience.\n\n<!-- markdownlint-disable-next-line no-emphasis-as-heading -->\n**Table of Contents**\n\n- [Overview](#overview)\n    - [Basic configuration example](#basic-configuration-example)\n    - [Advanced configuration example](#advanced-configuration-example)\n- [Source identifiers](#source-identifiers)\n- [Specify content with `sources`](#specify-content-with-sources)\n    - [Homebrew](#homebrew)\n        - [Additional notes about homebrew](#additional-notes-about-homebrew)\n    - [Reporting content errors to 5eTools](#reporting-content-errors-to-5etools)\n- [Specify target vault paths (`paths`)](#specify-target-vault-paths-paths)\n- [Refine content choices](#refine-content-choices)\n    - [Excluding content matching an `excludePattern`](#excluding-content-matching-an-excludepattern)\n    - [Excluding specific content with `exclude`](#excluding-specific-content-with-exclude)\n    - [Including specific content with `include`](#including-specific-content-with-include)\n- [Reprint behavior](#reprint-behavior)\n    - [Troubleshooting reprint behavior](#troubleshooting-reprint-behavior)\n- [Races as species](#races-as-species)\n- [Only emit referenced tables](#only-emit-referenced-tables)\n- [Use the dice roller plugin](#use-the-dice-roller-plugin)\n- [Render with Fantasy Statblocks](#render-with-fantasy-statblocks)\n- [Tag prefix](#tag-prefix)\n- [Templates](#templates)\n    - [Customizing templates](#customizing-templates)\n- [Images](#images)\n    - [Copying internal images](#copying-internal-images)\n    - [Copying external images](#copying-external-images)\n    - [Fallback paths](#fallback-paths)\n- [Customizing the default source](#customizing-the-default-source)\n- [Migrating `from`, `full-source`, and `convert`](#migrating-from-full-source-and-convert)\n\n## Overview\n\nThe CLI can be set up using JSON or YAML files. These files allow you to specify your preferences and can be used alongside or in place of command-line options. For examples of configuration file structures in both formats, see [examples/config](../examples/config).\n\n> [!NOTE]\n> 📝 JSON and YAML are both file formats for storing data in useful and human-readable ways.\n>\n> - JSON: If you want to know why the `{}` and `[]` are used in the ways that they are you can read about json *objects* and *arrays* [here](https://www.toolsqa.com/rest-assured/what-is-json/)).\n> - YAML: A format where indentation (spaces at the beginning of lines) is important. Learn about YAML's [specification](https://yaml.org/spec/1.2/spec.html).\n\nThe following examples are presented in JSON.\n\n> [!IMPORTANT]\n> Check your work! If you are getting errors reading your config file, *don't rely on your eyes,* **use a linter!**\n>\n> - JSON: <https://jsonlint.com/>\n> - YAML: <https://www.yamllint.com/>\n\n### Basic configuration example\n\nBelow is a straightforward `config.json` file. In this format, settings are noted in a `\"key\": \"value\"` structure.\n\n``` json\n{\n    \"sources\": {\n        \"book\": [\n            \"DMG\",\n            \"PHB\",\n            \"MM\"\n        ]\n    },\n    \"paths\": {\n        \"compendium\": \"z_compendium/\",\n        \"rules\": \"z_compendium/rules\"\n    }\n}\n```\n\nThis example performs two basic functions:\n\n1. **Select Input Sources:** The `sources` key lists the sources to be included, identified by their [source identifiers](#source-identifiers).\n2. **Define Vault Paths:** The [`paths`](#specify-target-vault-paths-paths) key sets the destination paths for the `compendium` and `rules` content. These paths are relative to the output directory set in the CLI command with `-o`.\n\n> [!WARNING]\n> **Windows Users**: Replace any `\\` with `/` in paths in JSON or YAML files\n\n### Advanced configuration example\n\nHere's a more comprehensive `config.json` file.\n\n```json\n{\n    \"sources\": {\n        \"adventure\": [\n            \"LMoP\",\n            \"LoX\"\n        ],\n        \"book\": [\n            \"PHB\"\n        ],\n        \"reference\": [\n            \"AI\",\n            \"DMG\",\n            \"TCE\",\n            \"ESK\",\n            \"DIP\",\n            \"XGE\",\n            \"FTD\",\n            \"MM\",\n            \"MTF\",\n            \"VGM\"\n        ],\n        \"homebrew\": [\n            \"homebrew/creature/MCDM Productions; Flee, Mortals!.json\"\n        ]\n    },\n    \"paths\": {\n        \"rules\": \"/compendium/rules/\"\n    },\n    \"excludePattern\": [\n        \"race\\\\|.*\\\\|dmg\"\n    ],\n    \"exclude\": [\n        \"monster|expert|dc\",\n        \"monster|expert|sdw\",\n        \"monster|expert|slw\"\n    ],\n    \"include\": [\n        \"race|changeling|mpmm\"\n    ],\n    \"reprintBehavior\": \"newest\",\n    \"useDiceRoller\": true,\n    \"tagPrefix\": \"ttrpg-cli\",\n    \"template\": {\n        \"background\": \"examples/templates/tools5e/images-background2md.txt\",\n        \"monster\": \"examples/templates/tools5e/monster2md-scores.txt\"\n    }\n}\n```\n\nAdditional capabilities:\n\n1. **Select input sources:** The [`sources`](#specify-content-with-sources) key is used to select included sources (full text from two adventures and a book, reference content from a slew of other official sources, and one [homebrew source](#homebrew)).\n2. **Define Vault Paths:** The [`path`](#specify-target-vault-paths-paths) sets the vault path destination for `rules` content.\n3. **Targeted exclusion:** [`excludePattern`](#excluding-content-matching-an-excludepattern) and [`exclude`](#excluding-specific-content-with-exclude) leaves out specific content.\n4. **Targeted inclusion:** The [`include`](#including-specific-content-with-include) specifies content that is *always included*.\n5. **[Reprint behavior](#reprint-behavior):** Only the latest/newest version of a resource should be emitted (this is the default).\n6. **Use the dice roller plugin:** The [`useDiceRoller`](#use-the-dice-roller-plugin) key enables the dice roller plugin.\n7. **Tag prefix:** The [`tagPrefix`](#tag-prefix) key sets the prefix for tags generated by the CLI.\n8. **Templates:** The [`template`](#templates) key specifies the templates to use for different types of content.\n\n> [!WARNING]\n> **Windows Users**: Replace any `\\` in your paths with '/' in your JSON and YAML files.\n\n## Source identifiers\n\n> 🚀 Remember: Only include content you legally own.\n\nSources in 5eTools and Pf2eTools are referenced by unique identifiers. Find the identifiers for your sources in the [Source Map](sourceMap.md).\n\nContent is classified as a `book` or `adventure` (shown as the third column in the source map). Use this classification when [specifying your sources](#specify-content-with-sources).\n\nSome sources are split into multiple files, in which case, you will need to specify each identifier separately. For example, *Tales from the Yawning Portal* is split into seven files. Content appears using any one of the seven (`TftYP-*`), in addition to `TftYP` for common content. If you want to include all of them, you will need to specify each identifier separately.\n\nIf you're expecting to see content from a book or adventure and it's not showing up, run the CLI with the `--index` option, and check the `all-index.json` file to see which source identifier you should be using.\n\n## Specify content with `sources`\n\n> 🔥 Version 3.x or SNAPSHOT ONLY. If you're using a 2.x version of the CLI, use [the legacy version](#migrating-from-full-source-and-convert)\n\nThe CLI can emit content from a source in two ways:\n\n- \"full text\": notes for all content and reference data from your sources.\n    When including the full text, use the `book`, `adventure`, or `homebrew` key as appropriate for the source material.\n- \"reference only\": only emit reference notes (spells, classes, etc.).\n    Use the `reference` key to include reference content from books or adventures.\n\nWith that in mind, specify your sources in this way:\n\n```json\n\"sources\": {\n    \"adventure\": [\n        \"WBtW\"\n    ],\n    \"book\": [\n        \"PHB\"\n    ],\n    \"reference\": [\n        \"MPMM\"\n    ]\n}\n```\n\nThe above example that will include full text for the *Player's Handbook* (a book, PHB) and *The Wild Beyond the Witchlight* (an adventure, WBtW), but will only create reference notes (backgrounds, cults/boons, races, traps/hazards) from *Mordenkainen Presents: Monsters of the Multiverse* (MPMM).\n\n> [!TIP]\n> You only need to list your source once.\n\n### Homebrew\n\n> [!TIP]\n> 🍺 *You only need the particular file you wish to import*.\n>\n> Homebrew data is different from the 5etools or Pf2eTools data. Each homebrew file is a complete reference. If you compare it to cooking: the 5etools and Pf2eTools mirror repositories are organized by ingredient (all of the carrots, all of the onions, ... ); homebrew data is organized by prepared meal / complete receipe.\n>\n> Support your content creators! Only use homebrew that you own.\n\nTo include Homebrew in your notes, specify the path to the homebrew json file in a `homebrew` section inside of `sources`.\n\nFor example, if you wanted to use Benjamin Huffman's popular homebrewed [Pugilist class](https://www.dmsguild.com/product/184921/The-Pugilist-Class):\n\n1. Download a copy of the [Pugilist json file](https://github.com/TheGiddyLimit/homebrew/blob/master/class/Benjamin%20Huffman%3B%20Pugilist.json).\n\n    Save this file to a well-known location on your computer. It is probably easiest if it sits next your 5eTools or Pf2eTools directory.\n\n2. Add the path to this file to a `homebrew` section under `sources`:\n\n    ```json\n    {\n      \"sources\": {\n        \"homebrew\": [\n            \"path/to/Benjamin Huffman; Pugilist.json\"\n        ]\n      }\n    }\n    ```\n\nIn the above example, `path/to/` is a placeholder. If you use a relative path, it will be resolved relative to the current working directory[^1]. An absolute path[^2] will also work.\n\nThere are a few ways to figure out the path to a file:\n\n- You may be able to drag and drop the file into the terminal window.\n- You may have the ability to right-click on the file and select \"Copy Path\".\n\n> [!WARNING]\n> **Windows Users**: Replace any `\\` with `/` in paths in JSON or YAML files\n\n#### Additional notes about homebrew\n\nHomebrew json files are not rigorously validated. There may be errors when importing.\nI've done what I can to make the errors clear, or to highlight the suspect json, but I can't catch everything.\n\nHere are some examples of what you may see, and how to fix them:\n\n- `Unable to find image 'img/bestiary/MM/Green Hag.jpg'` (or similar)\n\n    This kind of path refers to an \"internal\" (meaning part of the base 5e corpus of stuff) image. These paths are computed relative to a known base.\n\n    Recent releases of 5eTools use a different repository structure, see [Copying \"internal\" images](#copying-internal-images)), and images have, by and large, been converted from `.jpg` or `.png` to `.webp`.\n    Fixing this kind of error is usually a case of fixing the path.\n\n    - If you can fix the link yourself (change to `.webp` and guess the new location by removing `img/`), please [report it in 5eTools #brew-conversion](#reporting-content-errors-to-5etools).\n    - If you can't find the image that should be used instead, please [ask for CLI help](../README.md#where-to-find-help)and we'll help you find the right one.\n\n- `Unknown spell school Curse in sources[spell|ventus|wandsnwizards]`; similar for item types, item properties, conditions, skills, abilities, etc.\n\n    This kind of error could be caused by a missing companion file (check dependencies listed in the `meta` information at the top of the homebrew file) or a missing definition.\n\n    - If you find the missing definition (or it is already present in the homebrew file), and the error persists, please [ask for CLI help first](../README.md#where-to-find-help) so I can make sure there isn't a bug in the CLI preventing it from being read.\n    - If the data really is missing from the homebrew json, please [report it in 5eTools #brew-conversion](#reporting-content-errors-to-5etools).\n\n- You may see messages about missing fields or badly formed tables.\n\n    - If you can fix the error by fixing the json content, please [report it in 5eTools #brew-conversion](#reporting-content-errors-to-5etools).\n    - If you can't fix the error yourself, please [ask for CLI help first](../README.md#where-to-find-help) so I can make sure there isn't a bug in the CLI.\n\n### Reporting content errors to 5eTools\n\n> [!NOTE]\n> The lovely folks at 5eTools don't understand the CLI, and they don't need to. If you report an issue, keep the details focused on the JSON content (typo, missed definition, etc.). If you aren't sure, [ask for CLI help first](../README.md#where-to-find-help).\n\nIf you can fix the error by fixing the json content, please report the error in the 5eTools discord channel.\n\n1. Identify the homebrew source that contains the error in the [homebrew manager](https://wiki.tercept.net/en/5eTools/HelpPages/managebrew).\n2. Click the \"View Converters\" button on the right to find the converter(s).\n3. Tag them in the [#⁠brew-conversion](https://discord.com/channels/363680385336606740/493154206115430400) channel, describing what you fixed. If you can't find the right user to tag, or if no one is listed as a converter, post the report anyway but make sure you mention this!\n\nUse the following form for your report.\n\n```text\n**Brew:**   Write the name of the homebrew source here  \n**Converter:**   @converter's-discord-ID   \n**Issue:**   Describe the issue here in clear, concise terms (e.g. \"the Red-Spotted Gurgler is listed as having AC 15, when it should be 16\")  \n**Steps for reproduction:**   If you have to do something specific to make the error appear, describe them here. \n> I go to X and click on Y \n> I expected Z, but instead ...\n```\n\nFor that last part, you may need to do some digging. Do not report the error using CLI exception messages. Stick to the observed missing links or errors in the data.\n\n## Specify target vault paths (`paths`)\n\nThe `paths` key specifies vault path for generated content.\n\n- New directories are made if they aren't already present.\n- Paths are relative to the CLI's designated output location (`-o`), which correlates to the root of your Obsidian vault.\n\n**Example:**\n\n```json\n  \"paths\": {\n    \"compendium\": \"/compendium/\",\n    \"rules\": \"/rules/\"\n  }\n```\n\n> [!TIP]\n> The leading slash is optional. It marks a path starting from the root of your Obsidian vault.\n\n5eTools and Pf2eTools content is organized differently, but in general, information is organized as follows:\n\n- `compendium`: backgrounds, classes, items, spells, monsters, etc.\n- `rules`: conditions, weapon properties, variant rules, etc.\n\n> [!WARNING]\n> Do not reorganize or edit the generated content. Tuck generated content away in your vault and use it as read-only reference material. It should be cheap and easy to re-run the tool (add more content, errata, etc.). See [Recommendations](../README.md#recommendations-for-using-the-cli) for more information.\n\n## Refine content choices\n\nYou can use the following configuration to exclude or include specific data.\n\nJust as source material has an identifier, so does each piece of data. The *Monster Manual* has the identifier `MM`. Each monster in the *Monster Manual* has its own key, such as `monster|black dragon wyrmling|mm` or `item|drow +1 armor|mm`.\n\nThe CLI `--index` option compiles two lists of data keys:\n\n- `all-index.json`: Lists all discovered data keys.\n- `src-index.json`: Lists the data keys after source filters (`adventure`, `book`, `reference`, and the config options below) have been applied.\n\n> [!NOTE]\n> **Emitted types**:\n> You can check some common emitted types to include or exclude [here](#customizing-the-default-source).\n\n### Excluding content matching an `excludePattern`\n\nThis option allows you to exclude data entries based on regular expression matching patterns.\n\nNote: A pipe (`|`) is a special character in regular expressions, and must be escaped.\n\n- JSON\n\n    ```json\n    \"excludePattern\": [\n        \"race\\\\|.*\\\\|dmg\"\n    ]\n    ```\n\n- YAML\n\n    ```yaml\n    excludePattern:\n      - race\\|.*\\|dmg\n    ```\n\n### Excluding specific content with `exclude`\n\nSpecify the data keys you want to omit.\n\n```json\n\"exclude\": [\n    \"monster|expert|dc\",\n    ...\n]\n```\n\n### Including specific content with `include`\n\nSpecify the data keys you want to include.\n\n```json\n\"include\": [\n    \"race|changeling|mpmm\"\n]\n```\n\nThis approach is ideal for content acquired in parts, like individual items from D&D Beyond.\n\n## Reprint behavior\n\n> 🔥 Version 3.x or SNAPSHOT ONLY.\n\nContent is often reprinted or updated in later sources or editions. This setting lets you control how reprinted or revised content is handled when generating notes.\n\n``` json\n  \"reprintBehavior\": \"newest\"\n```\n\nThis setting has 3 possible values:\n\n- **`newest`** (default): Only includes notes for the most recent version of reprinted content.\n- **`edition`**: Focuses on preserving content across incompatible editions (especially for 5e rules).\n\n    Example: The edition check will preserve 2014 edition-specific class and subclass definitions. Other resources (that are not different across editions) will follow the reprints to include new content.\n\n- **`all`**: Includes notes for all reprinted versions from enabled sources\n\nIn most cases, you will get the most recent version of the resource that is included, as most resources do not have substantial changes across editions.\n\nFor example, `trap|pits|dmg` is reprinted as `trap|hidden pit|xdmg`. If both versions are included by your configuration, you will only get a note for the `XDMG` version unless `reprintBehavior` is set to `all` or you have an explicit include rule that preserves the `DMG` version (in which case, you'll get both).\n\n### Troubleshooting reprint behavior\n\nIf the behavior isn’t what you expect, run with the --log option and check the log file.\nThe log will show whether a specific key was kept or dropped and explain why.\n\nTo ensure a specific resource is included, add its key to the [`include` filter](#including-specific-content-with-include) instead of relying on reprint behavior.\n\n## Races as species\n\nIf you prefer the term \"species\" over \"race\" (as used in newer D&D editions), set `racesAsSpecies` to `true`.\n\n``` json\n  \"racesAsSpecies\": true\n```\n\nThis changes the output directory from `races/` to `species/`, tags from `race/...` to `species/...`, and the CSS class from `json5e-race` to `json5e-species`. It does not affect internal indexing or source data processing.\n\n## Only emit referenced tables\n\nBy default, the CLI emits all table notes from included sources. Set `onlyReferencedTables` to `true` to restrict output to tables that are actually linked from other included content (monsters, spells, adventures, etc.).\n\n``` json\n  \"onlyReferencedTables\": true\n```\n\nWhen enabled, a table note is written only if:\n\n- something in the rendered output links to it (via a `{@table}` tag or inline table matching), **or**\n- it is explicitly named in an [`include` filter](#including-specific-content-with-include).\n\nUnreferenced tables are logged as `(drop | unreferenced)` when run with `--log`.\n\n## Use the dice roller plugin\n\nThe CLI can generate notes that include inline dice rolls. To enable this feature, set the `useDiceRoller` attribute to `true`.\n\n## Render with Fantasy Statblocks\n\nIf you are using the Fantasy Statblocks plugin to render your statblocks, set `yamlStatblocks` to `true`. This will remove backticks and other formatting from statblock text.\n\n## Tag prefix\n\nThe `tagPrefix` key sets the prefix for tags generated by the CLI. This is useful if you want to distinguish between tags generated by the CLI and tags you've created yourself.\n\nFor example, the CLI generates tags like `compendium/src/phb` and `spell/level/1`. If you set `tagPrefix` to `5e-cli`, the tags will be `5e-cli/compendium/src/phb` and `5e-cli/spell/level/1`.\n\n## Templates\n\nThe CLI uses the [Qute Templating Engine](https://quarkus.io/guides/qute) to render markdown output. Use the `template` attribute in your configuration file to specify the templates you want to use for different types of content.\n\n``` json\n  \"template\": {\n    \"background\": \"examples/templates/tools5e/images-background2md.txt\",\n    \"monster\": \"examples/templates/tools5e/monster2md-scores.txt\"\n  }\n```\n\n- **Default templates** are included in the `-examples.zip` file from the release, or can be viewed in the [src/main/resources/templates](../src/main/resources/templates) directory.\n- **Additional templates** are available in the [examples/templates](../examples/templates) directory.\n\n> [!TIP] The key used to specify a template corresponds to the type of template being used. You can find the list of valid template keys in the [source code](../src/main/resources/convertData.json) (look for `templateKeys`).\n\n- Valid templates for 5etools: `background`, `class`, `deck`, `deity`, `feat`, `hazard`, `index.txt`, `item`, `monster`, `note`, `object`, `psionic`, `race`, `reward`, `spell`, `subclass`, `vehicle`.\n- Valid templates for Pf2eTools: `ability`, `action`, `affliction`, `archetype`, `background`, `book`, `deity`, `feat`, `hazard`, `inline-ability`, `inline-affliction`, `inline-attack`, `item`, `note`, `ritual`, `spell`, `trait`.\n\n### Customizing templates\n\nDocumentation is generated for [**template attributes**](./templates/).\n\nNot everything is customizable. Some indenting, organizing, formatting, and linking is easier to do consistently while rendering big blobs of text.\n\nSee the [examples templates](../examples/templates) for reference.\n\n## Images\n\nThe CLI can copy images referenced in the content to your vault. This is useful if you want to use the content offline or if you want to ensure that images are available in your vault.\n\n- Internal images are part of the 5eTools or Pf2e tools corpus of content. They are referenced by computed path (like tokens) or by media references marked as \"internal\".\n- \"External\" images are usually marked in the Json source as \"external\" and are referenced by a URL.\n\n### Copying internal images\n\n5eTools internal images are stored in a separate repository.\n\n**By default, images are not downloaded**. Links reference the remote location, requiring internet access to view. This is the safest, fastest option.\n\nTo copy internal images into your vault:\n\n- **Option 1: Download on demand**\n\n    ```json\n    \"images\": {\n        \"copyInternal\": true\n    }\n    ```\n\n    The CLI downloads each image individually as needed. This is many separate requests and may be slow.\n\n- **Option 2: Use a local clone (faster)**\n\n    ```json\n    \"images\": {\n        \"copyInternal\": true,\n        \"internalRoot\": \"5etools-img\"\n    }\n    ```\n\n    The CLI reads images from a local directory. Relative paths resolve from the current working directory[^1]; absolute paths[^2] also work. You'll get an error if the directory doesn't exist.\n\n> [!NOTE]\n> If not copying images, delete both `copyInternal` and `internalRoot` attributes.\n>\n> If using `internalRoot`, ensure the path exists. `http` or `file` URLs work best; otherwise the CLI creates a `file` link from the specified path.\n>\n> Without copying, images must be accessible from your vault to display in Obsidian.\n\n### Copying external images\n\nExternal images are marked as \"external\" in the JSON source and typically use `http://`, `https://`, or `file://`[^3] URLs.\n\n**By default, external images are not downloaded**. Links reference the remote location, requiring internet access to view. This is the safest, fastest option.\n\nTo copy external images into your vault:\n\n```json\n\"images\": {\n    \"copyExternal\": true\n}\n```\n\nThe CLI will download each external image it hasn't seen before.\n\n### Fallback paths\n\n🧪 Experimental feature. Report issues if problems occur.\n\nIf an image reference is broken or fails to copy, you can specify a fallback path:\n\n```json\n\"images\": {\n    \"fallbackPaths\": {\n        \"img/bestiary/MM/Green Hag.jpg\": \"img/bestiary/MM/Green Hag.webp\"\n    }\n}\n```\n\n- **Key** (original path): Must match exactly what the JSON source specifies. For external/homebrew images, check the JSON source. Internal images may be harder to identify.\n- **Value** (replacement path): Must be a valid local file path[^2] or URL[^3].\n\n## Customizing the default source\n\n> [!WARNING]\n> 🔥 You can truly make a mess with this setting.\n> Change these values with care, and inspect the result carefully in a test vault.\n>\n> - This will change generated file names. It will break links.\n> - If you have content generated with a different defaultSource configuration,\n>   completely remove it before copying freshly generated content into your vault ^[You could also use something like rsync that will remove extraneous files].\n\nIf you're only running 5e 2024 content (as an example), you may want to change the \"default\" source for items, etc. to be XPHB or XDMG or XMM, such that those files do not have the additional notations (e.g. the file name suffix, or the additional source designation in the monster name).\n\nChange the default source for an index type in the `sources` block:\n\n```json\n\"sources\": {\n    \"defaultSource\": {\n        \"monster\": \"XMM\"\n    }\n}\n```\n\nCreate a map of a content type to the default source. In general, this is the same key you would use to assign a template. If you open up the [generated index](#source-identifiers), the first segment of a key is its type, for example, `trap|collapsing roof|dmg` is a `trap`. Some types are grouped because of tight inter-relationships, like cards and decks.\n\n| Emitted type          | Default Source | Includes (note) |\n|-----------------------|----------------|----------|\n| background            | PHB | |\n| classtype             | PHB | subclass, class feature, subclass feature |\n| deck                  | DMG | card |\n| deity                 | PHB | |\n| disease               | DMG | |\n| facility              | XDMG | (bastion) |\n| feat                  | PHB | |\n| item                  | DMG | item group, magic variant |\n| monster               | MM | legendary group (bestiary) |\n| object                | DMG | |\n| optfeature            | PHB | |\n| psionic               | UATheMysticClass | |\n| race                  | PHB | subrace (species) |\n| reward                | DMG | |\n| spell                 | PHB | |\n| table                 | DMG | table group |\n| trap                  | DMG | hazard |\n| variantrule           | DMG | |\n| vehicle               | GoS | |\n\n## Migrating `from`, `full-source`, and `convert`\n\nOlder configurations looked a little different. Updating to the new format should be straightforward.\n\nFor comparison, the following examples of older configurations use the same values as the [example above](#specify-content-with-sources).\n\n```json\n\"from\": [\n    \"MPMM\"\n],\n\"full-source\": {\n    \"adventure\": [\n        \"WBtW\"\n    ],\n    \"book\": [\n        \"PHB\"\n    ],\n    \"homebrew\": [\n        ...\n    ]\n}\n```\n\nOR\n\n```json\n\"from\": [\n    \"MPMM\"\n],\n\"convert\": {\n    \"adventure\": [\n        \"WBtW\"\n    ],\n    \"book\": [\n        \"PHB\"\n    ],\n    \"homebrew\": [\n        ...\n    ]\n}\n```\n\n[^1]: The working directory is the directory you were in (in the terminal) when you launched the CLI. See <https://en.wikipedia.org/wiki/Working_directory> for more information\n[^2]: Example/explanation of absolute vs. relative path: <https://stackoverflow.com/a/10288252>. If you're using relative paths with the CLI, they should be relative to the working directory (see [^1]).\n[^3]: A URL is a uniform resource locator, more information at <https://en.wikipedia.org/wiki/URL>.\n"
  },
  {
    "path": "docs/sourceMap.md",
    "content": "# Source mapping\n\n- [5etools](#source-name-mapping-for-5etools)\n- [Pf2eTools](#source-name-mapping-for-pf2etools)\n\nHere is the name/abbreviation mapping for source materials.\n\n_Support content creators. Only use or include sources that you own._\n\n## Source name mapping for 5etools\n\n- **2014** (sources/reference): \"srd\", \"basicrules\"\n- **2024** (sources/reference): \"srd52\", \"basicRules2024\"\n\n### 5eTools Abbreviations to long name\n\n| Abbreviation | Long name | Type |\n|--------------|-----------|-------|\n| AAG | Astral Adventurer's Guide | book |\n| AATM | Adventure Atlas: The Mortuary | book |\n| ABH | Astarion's Book of Hungers | book |\n| AI | Acquisitions Incorporated | book |\n| AL | Adventurers' League | book |\n| ALCoS | Adventurers League: Curse of Strahd | reference |\n| ALEE | Adventurers League: Elemental Evil | reference |\n| ALRoD | Adventurers League: Rage of Demons | reference |\n| AWM | Adventure with Muk | book |\n| AZfyT | A Zib for your Thoughts | adventure |\n| AitFR | Adventures in the Forgotten Realms | reference |\n| AitFR-AVT | Adventures in the Forgotten Realms: A Verdant Tomb | adventure |\n| AitFR-DN | Adventures in the Forgotten Realms: Deepest Night | adventure |\n| AitFR-FCD | Adventures in the Forgotten Realms: From Cyan Depths | adventure |\n| AitFR-ISF | Adventures in the Forgotten Realms: In Scarlet Flames | adventure |\n| AitFR-THP | Adventures in the Forgotten Realms: The Hidden Page | adventure |\n| BAM | Boo's Astral Menagerie | book |\n| BGDIA | Baldur's Gate: Descent Into Avernus | adventure |\n| BGG | Bigby Presents: Glory of the Giants | book |\n| BMT | The Book of Many Things | book |\n| BQGT | Borderlands Quest: Goblin Trouble | adventure |\n| CM | Candlekeep Mysteries | adventure |\n| CRCotN | Critical Role: Call of the Netherdeep | adventure |\n| CaBoMP | Crochet: A Book of Many Patterns | book |\n| CoA | Chains of Asmodeus | adventure |\n| CoS | Curse of Strahd | adventure |\n| DC | Divine Contention | adventure |\n| DD | Dangerous Designs | adventure |\n| DIP | Dragon of Icespire Peak | adventure |\n| DMG | Dungeon Master's Guide | book |\n| DMTCRG | The Deck of Many Things: Card Reference Guide | book |\n| DSotDQ | Dragonlance: Shadow of the Dragon Queen | adventure |\n| DitLCoT | Descent into the Lost Caverns of Tsojcanth | adventure |\n| DoD | Domains of Delight | book |\n| DoDk | Dungeons of Drakkenheim | reference |\n| DoSI | Dragons of Stormwreck Isle | adventure |\n| DrDe | Dragon Delves | reference |\n| DrDe-ACfaS | A Copper for a Song | adventure |\n| DrDe-BD | A Copper for a Song | adventure |\n| DrDe-BtS | Before the Storm | adventure |\n| DrDe-DaS | Death at Sunset | adventure |\n| DrDe-DotSC | Dragons of the Sandstone City | adventure |\n| DrDe-FWtVC | For Whom the Void Calls | adventure |\n| DrDe-SD | Shivering Death | adventure |\n| DrDe-TDoN | The Dragon of Najkir | adventure |\n| DrDe-TFV | The Forbidden Vale | adventure |\n| DrDe-TWoO | The Will of Orcus | adventure |\n| EEPC | Elemental Evil Player's Companion | reference |\n| EET | Elemental Evil: Trinkets | reference |\n| EFA | Eberron: Forge of the Artificer | book |\n| EFR | Eberron: Forgotten Relics | adventure |\n| EGW | Explorer's Guide to Wildemount | book |\n| EGW_DD | Dangerous Designs | reference |\n| EGW_FS | Frozen Sick | reference |\n| EGW_ToR | Tide of Retribution | reference |\n| EGW_US | Unwelcome Spirits | reference |\n| ERLW | Eberron: Rising from the Last War | book |\n| ESK | Essentials Kit | reference |\n| FFotR | Fated Flight of the Recluse | adventure |\n| FRAiF | Forgotten Realms: Adventures in Faerûn | book |\n| FRAiF-TLLoL | Forgotten Realms: The Lost Library of Lethchauntos | adventure |\n| FRHoF | Forgotten Realms: Heroes of Faerûn | book |\n| FS | Frozen Sick | adventure |\n| FTD | Fizban's Treasury of Dragons | book |\n| GGR | Guildmasters' Guide to Ravnica | book |\n| GHLoE | Grim Hollow: Lairs of Etharis | reference |\n| GoS | Ghosts of Saltmarsh | adventure |\n| GotSF | Giants of the Star Forge | adventure |\n| HAT-LMI | Honor Among Thieves: Legendary Magic Items | reference |\n| HAT-TG | Honor Among Thieves: Thieves' Gallery | book |\n| HBTD | Hold Back The Dead | adventure |\n| HF | Heroes' Feast | book |\n| HFDoMM | Heroes' Feast: The Deck of Many Morsels | reference |\n| HFFotM | Heroes' Feast Flavors of the Multiverse | book |\n| HFStCM | Heroes' Feast: Saving the Children's Menu | adventure |\n| HWAitW | Humblewood: Adventure in the Wood | reference |\n| HWCS | Humblewood Campaign Setting | reference |\n| HftT | Hunt for the Thessalhydra | adventure |\n| HoL | The House of Lament | adventure |\n| HotB | Heroes of the Borderlands | adventure |\n| HotDQ | Hoard of the Dragon Queen | adventure |\n| IDRotF | Icewind Dale: Rime of the Frostmaiden | adventure |\n| IMR | Infernal Machine Rebuild | adventure |\n| JttRC | Journeys through the Radiant Citadel | adventure |\n| KKW | Krenko's Way | adventure |\n| KftGV | Keys from the Golden Vault | adventure |\n| LFL | Lorwyn: First Light | book |\n| LK | Lightning Keep | adventure |\n| LLK | Lost Laboratory of Kwalish | adventure |\n| LMoP | Lost Mine of Phandelver | adventure |\n| LR | Locathah Rising | adventure |\n| LRDT | Red Dragon's Tale: A LEGO Adventure | adventure |\n| LoX | Light of Xaryxis | adventure |\n| MCV1SC | Monstrous Compendium Volume 1: Spelljammer Creatures | reference |\n| MCV2DC | Monstrous Compendium Volume 2: Dragonlance Creatures | reference |\n| MCV3MC | Monstrous Compendium Volume 3: Minecraft Creatures | reference |\n| MCV4EC | Monstrous Compendium Volume 4: Eldraine Creatures | book |\n| MFF | Mordenkainen's Fiendish Folio | reference |\n| MGELFT | Muk's Guide To Everything He Learned From Tasha | book |\n| MM | Monster Manual | book |\n| MOT | Mythic Odysseys of Theros | book |\n| MPMM | Mordenkainen Presents: Monsters of the Multiverse | book |\n| MPP | Morte's Planar Parade | book |\n| MTF | Mordenkainen's Tome of Foes | book |\n| MaBJoV | Minsc and Boo's Journal of Villainy | book |\n| MisMV1 | Misplaced Monsters: Volume 1 | reference |\n| NF | Netheril's Fall | book |\n| NRH | NERDS Restoring Harmony | reference |\n| NRH-ASS | NERDS Restoring Harmony: A Sticky Situation | adventure |\n| NRH-AT | NERDS Restoring Harmony: Adventure Together | adventure |\n| NRH-AVitW | NERDS Restoring Harmony: A Voice in the Wilderness | adventure |\n| NRH-AWoL | NERDS Restoring Harmony: A Web of Lies | adventure |\n| NRH-CoI | NERDS Restoring Harmony: Circus of Illusions | adventure |\n| NRH-TCMC | NERDS Restoring Harmony: The Candy Mountain Caper | adventure |\n| NRH-TLT | NERDS Restoring Harmony: The Lost Tomb | adventure |\n| OGA | One Grung Above | book |\n| OoW | The Orrery of the Wanderer | adventure |\n| OotA | Out of the Abyss | adventure |\n| PHB | Player's Handbook | book |\n| PSA | Plane Shift: Amonkhet | reference |\n| PSD | Plane Shift: Dominaria | reference |\n| PSI | Plane Shift: Innistrad | reference |\n| PSK | Plane Shift: Kaladesh | reference |\n| PSX | Plane Shift: Ixalan | reference |\n| PSZ | Plane Shift: Zendikar | reference |\n| PaBTSO | Phandelver and Below: The Shattered Obelisk | adventure |\n| PaF | Puncheons and Flagons | book |\n| PiP | Peril in Pinegrove | adventure |\n| PotA | Princes of the Apocalypse | adventure |\n| QftIS | Quests from the Infinite Staircase | adventure |\n| RMBRE | The Lost Dungeon of Rickedness: Big Rick Energy | adventure |\n| RMR | Dungeons & Dragons vs. Rick and Morty: Basic Rules | book |\n| RoT | The Rise of Tiamat | adventure |\n| RoTOS | The Rise of Tiamat Online Supplement | reference |\n| RtG | Return to Glory | adventure |\n| SAC | Sage Advice Compendium | book |\n| SADS | Sapphire Anniversary Dice Set | reference |\n| SAiS | Spelljammer: Adventures in Space | reference |\n| SCAG | Sword Coast Adventurer's Guide | book |\n| SCC | Strixhaven: A Curriculum of Chaos | book |\n| SCC-ARiR | A Reckoning in Ruins | adventure |\n| SCC-CK | Campus Kerfuffle | adventure |\n| SCC-HfMT | Hunt for Mage Tower | adventure |\n| SCC-TMM | The Magister's Masquerade | adventure |\n| SCREEN | Dungeon Master's Screen | reference |\n| SCREEN_DUNGEON_KIT | Dungeon Master's Screen: Dungeon Kit | reference |\n| SCREEN_SPELLJAMMER | Dungeon Master's Screen: Spelljammer | reference |\n| SCREEN_WILDERNESS_KIT | Dungeon Master's Screen: Wilderness Kit | reference |\n| SDW | Sleeping Dragon's Wake | adventure |\n| SKT | Storm King's Thunder | adventure |\n| SLW | Storm Lord's Wrath | adventure |\n| SatO | Sigil and the Outlands | book |\n| ScoEE | Scions of Elemental Evil | adventure |\n| SjA | Spelljammer Academy | adventure |\n| TCE | Tasha's Cauldron of Everything | book |\n| TD | Tarot Deck | book |\n| TLK | The Lost Kenku | adventure |\n| TTP | The Tortle Package | adventure |\n| TftYP | Tales from the Yawning Portal | reference |\n| TftYP-AtG | Tales from the Yawning Portal: Against the Giants | adventure |\n| TftYP-DiT | Tales from the Yawning Portal: Dead in Thay | adventure |\n| TftYP-TFoF | Tales from the Yawning Portal: The Forge of Fury | adventure |\n| TftYP-THSoT | Tales from the Yawning Portal: The Hidden Shrine of Tamoachan | adventure |\n| TftYP-TSC | Tales from the Yawning Portal: The Sunless Citadel | adventure |\n| TftYP-ToH | Tales from the Yawning Portal: Tomb of Horrors | adventure |\n| TftYP-WPM | Tales from the Yawning Portal: White Plume Mountain | adventure |\n| ToA | Tomb of Annihilation | adventure |\n| ToB1-2023 | Tome of Beasts 1 (2023 Edition) | reference |\n| ToD | Tyranny of Dragons | reference |\n| ToFW | Turn of Fortune's Wheel | adventure |\n| ToR | Tide of Retribution | adventure |\n| UATMC | Unearthed Arcana: The Mystic Class | reference |\n| US | Unwelcome Spirits | adventure |\n| UtHftLH | Uni and the Hunt for the Lost Horn | adventure |\n| VD | Vecna Dossier | reference |\n| VEoR | Vecna: Eve of Ruin | adventure |\n| VGM | Volo's Guide to Monsters | book |\n| VNotEE | Vecna: Nest of the Eldritch Eye | adventure |\n| VRGR | Van Richten's Guide to Ravenloft | book |\n| WBtW | The Wild Beyond the Witchlight | adventure |\n| WDH | Waterdeep: Dragon Heist | adventure |\n| WDMM | Waterdeep: Dungeon of the Mad Mage | adventure |\n| WttHC | Stranger Things: Welcome to the Hellfire Club | adventure |\n| XDMG | Dungeon Master's Guide (2024) | book |\n| XGE | Xanathar's Guide to Everything | book |\n| XMM | Monster Manual (2024) | book |\n| XMtS | X Marks the Spot | adventure |\n| XPHB | Player's Handbook (2024) | book |\n| XSAC | Sage Advice Compendium (2025) | book |\n| XScreen | Dungeon Master's Screen (2024) | book |\n\n### 5eTools Alternate abbreviation mapping\n\nYou may see these abbreviations referenced in source material, this is how they map to sources listed above.\n\n| Abbreviation | Alias     |\n|--------------|-----------|\n| ALCurseOfStrahd | ALCoS |\n| ALElementalEvil | ALEE |\n| ALRageOfDemons | ALRoD |\n| HEROES_FEAST | HF |\n| PS-A | PSA |\n| PS-D | PSD |\n| PS-I | PSI |\n| PS-K | PSK |\n| PS-X | PSX |\n| PS-Z | PSZ |\n| SCC_ARiR | SCC-ARir |\n| SCC_CK | SCC-CK |\n| SCC_HfMT | SCC-HfMT |\n| SCC_TMM | SCC-TMM |\n| Screen | SCREEN |\n| ScreenDungeonKit | SCREEN_DUNGEON_KIT |\n| ScreenSpelljammer | SRC_SCREEN_SPELLJAMMER |\n| ScreenWildernessKit | SCREEN_WILDERNESS_KIT |\n| TYP | TftYP |\n| TYP_AtG | TftYP-AtG |\n| TYP_DiT | TftYP-DiT |\n| TYP_TFoF | TftYP-TFoF |\n| TYP_THSoT | TftYP-THSoT |\n| TYP_TSC | TftYP-TSC |\n| TYP_ToH | TftYP-ToHs |\n| TYP_WPM | TftYP-WPM |\n| UATheMysticClass | UATMC |\n| freeRules2024 | basicRules2024 |\n\n## Source name mapping for Pf2eTools\n\n### Pf2eTools Abbreviations to long name\n\n| Abbreviation | Long name |\n|--------------|-----------|\n| 7DfS0 | Seven Dooms for Sandpoint Player's Guide |\n| AAWS | Azarketi Ancestry Web Supplement |\n| AFFM | A Few Flowers More |\n| AFoF | A Fistful of Flowers |\n| APG | Advanced Player's Guide |\n| AV0 | Abomination Vaults Player's Guide |\n| AV1 | Abomination Vaults #1: Ruins of Gauntlight |\n| AV2 | Abomination Vaults #2: Hands of the Devil |\n| AV3 | Abomination Vaults #3: Eyes of Empty Death |\n| AVH | Abomination Vaults Hardcover |\n| AoA0 | Age of Ashes Player's Guide |\n| AoA1 | Age of Ashes #1: Hellknight Hill |\n| AoA2 | Age of Ashes #2: Cult of Cinders |\n| AoA3 | Age of Ashes #3: Tomorrow Must Burn |\n| AoA4 | Age of Ashes #4: Fires of the Haunted City |\n| AoA5 | Age of Ashes #5: Against the Scarlet Triad |\n| AoA6 | Age of Ashes #6: Broken Promises |\n| AoE0 | Agents of Edgewatch Player's Guide |\n| AoE1 | Agents of Edgewatch #1: Devil at the Dreaming Palace |\n| AoE2 | Agents of Edgewatch #2: Sixty Feet Under |\n| AoE3 | Agents of Edgewatch #3: All or Nothing |\n| AoE4 | Agents of Edgewatch #4: Assault on Hunting Lodge Seven |\n| AoE5 | Agents of Edgewatch #5: Belly of the Black Whale |\n| AoE6 | Agents of Edgewatch #6: Ruins of the Radiant Siege |\n| B1 | Bestiary |\n| B2 | Bestiary 2 |\n| B3 | Bestiary 3 |\n| BB | Beginner Box |\n| BL0 | Blood Lords Player's Guide |\n| BL1 | Blood Lords #1: Zombie Feast |\n| BL2 | Blood Lords #2: Graveclaw |\n| BL3 | Blood Lords #3: Field of Maidens |\n| BL4 | Blood Lords #4: The Ghouls Hunger |\n| BL5 | Blood Lords #5: A Taste of Ashes |\n| BL6 | Blood Lords #6: Ghost King's Rage |\n| BotD | Book of the Dead |\n| CC0 | Curtain Call Player's Guide |\n| CFD | Critical Fumble Deck |\n| CHD | Critical Hit Deck |\n| CRB | Core Rulebook |\n| CotT | Claws of the Tyrant |\n| DA | Dark Archive |\n| DaLl | Dinner at Lionlodge |\n| EC0 | Extinction Curse Player's Guide |\n| EC1 | Extinction Curse #1: The Show Must Go On |\n| EC2 | Extinction Curse #2: Legacy of the Lost God |\n| EC3 | Extinction Curse #3: Life's Long Shadows |\n| EC4 | Extinction Curse #4: Siege of the Dinosaurs |\n| EC5 | Extinction Curse #5: Lord of the Black Sands |\n| EC6 | Extinction Curse #6: The Apocalypse Prophet |\n| FRP0 | Fists of the Ruby Phoenix Player's Guide |\n| FRP1 | Fists of the Ruby Phoenix #1: Despair on Danger Island |\n| FRP2 | Fists of the Ruby Phoenix #2: Ready? Fight! |\n| FRP3 | Fists of the Ruby Phoenix #3: King of the Mountain |\n| FoP | The Fall of Plaguestone |\n| G&G | Guns & Gears |\n| GMG | Gamemastery Guide |\n| GW0 | Gatewalkers Player's Guide |\n| GW1 | Gatewalkers #1: The Seventh Arch |\n| GW2 | Gatewalkers #2: They Watched the Stars |\n| GW3 | Gatewalkers #3: Dreamers of the Nameless Spires |\n| HPD | Hero Point Deck |\n| HStR | Head-Shot the Rot |\n| HotW | Howl of the Wild |\n| LOACLO | Lost Omens: Absalom, City of Lost Omens |\n| LOAG | Lost Omens: Ancestry Guide |\n| LOCG | Lost Omens: Character Guide |\n| LODM | Lost Omens: Divine Mysteries |\n| LOGM | Lost Omens: Gods & Magic |\n| LOGMWS | Lost Omens: Gods & Magic Web Supplement |\n| LOHh | Lost Omens: Highhelm |\n| LOIL | Lost Omens: Impossible Lands |\n| LOKL | Lost Omens: Knights of Lastwall |\n| LOL | Lost Omens: Legends |\n| LOME | Lost Omens: The Mwangi Expanse |\n| LOMM | Lost Omens: Monsters of Myth |\n| LOPSG | Lost Omens: Pathfinder Society Guide |\n| LORA | Lost Omens: Rival Academies |\n| LOSK | Lost Omens: Shining Kingdoms |\n| LOTG | Lost Omens: Travel Guide |\n| LOTGB | Lost Omens: The Grand Bazaar |\n| LOTXWG | Lost Omens: Tian Xia World Guide |\n| LOWG | Lost Omens: World Guide |\n| LTiBA | Little Trouble in Big Absalom |\n| MS0 | Myth-Speaker Player's Guide |\n| Mal | Malevolence |\n| MotM | Mark of the Mantis |\n| NGD | Night of the Gray Death |\n| OoA0 | Outlaws of Alkenstar Player's Guide |\n| OoA1 | Outlaws of Alkenstar #1: Punks in a Powder Keg |\n| OoA2 | Outlaws of Alkenstar #2: Cradle of Quartz |\n| OoA3 | Outlaws of Alkenstar #3: The Smoking Gun |\n| PC1 | Player Core |\n| PC2 | Player Core 2 |\n| PFUM | PATHFINDER: FUMBUS! |\n| POS1 | Pathfinder One-Shot: Sundered Waves |\n| QFF0 | Quest for the Frozen Flame Player's Guide |\n| QFF1 | Quest for the Frozen Flame #1: Broken Tusk Moon |\n| QFF2 | Quest for the Frozen Flame #2: Lost Mammoth Valley |\n| QFF3 | Quest for the Frozen Flame #3: Burning Tundra |\n| RoE | Rage of Elements |\n| RotR0 | Revenge of the Runelords Player's Guide |\n| Rust | Rusthenge |\n| SF0 | Stolen Fate Player's Guide |\n| SF1 | Stolen Fate #1: The Choosing |\n| SF2 | Stolen Fate #2: The Destiny War |\n| SF3 | Stolen Fate #3: Worst of All Possible Worlds |\n| SKT0 | Sky King's Tomb Player's Guide |\n| SW0 | Spore War Player's Guide |\n| SaS | Shadows at Sundown |\n| Sli | The Slithering |\n| SoB0 | Shades of Blood Player's Guide |\n| SoG0 | Season of Ghosts Player's Guide |\n| SoG1 | Season of Ghosts #1: The Summer That Never Was |\n| SoG2 | Season of Ghosts #2: Let the Leaves Fall |\n| SoG3 | Season of Ghosts #3: No Breath to Cry |\n| SoG4 | Season of Ghosts #4: To Bloom Below the Web |\n| SoM | Secrets of Magic |\n| SoT0 | Strength of Thousands Player's Guide |\n| SoT1 | Strength of Thousands #1: Kindled Magic |\n| SoT2 | Strength of Thousands #2: Spoken on the Song Wind |\n| SoT3 | Strength of Thousands #3: Hurricane's Howl |\n| SoT4 | Strength of Thousands #4: Secrets of the Temple-City |\n| SoT5 | Strength of Thousands #5: Doorway to the Red Star |\n| SoT6 | Strength of Thousands #6: Shadows of the Ancients |\n| TEC | The Enmity Cycle |\n| TV | Treasure Vault |\n| TaL | Torment and Legacy |\n| TiO | Troubles in Otari |\n| ToK | Threshold of Knowledge |\n| TotT0 | Triumph of the Tusk Player's Guide |\n| WoI | War of Immortals |\n| WoW0 | Wardens of Wildwood Player's Guide |\n| WoW1 | Wardens of Wildwood #1: Pactbreaker |\n| WoW2 | Wardens of Wildwood #2: Severed at the Root |\n| WoW3 | Wardens of Wildwood #3: Shepherd of Decay |\n| WtD1 | Wake the Dead #1 |\n| WtD2 | Wake the Dead #2 |\n| WtD3 | Wake the Dead #3 |\n| WtD4 | Wake the Dead #4 |\n| WtD5 | Wake the Dead #5 |\n\n### Pf2eTools Alternate abbreviation mapping\n\nYou may see these abbreviations referenced in source material, this is how they map to sources listed above.\n\n| Abbreviation | Alias     |\n|--------------|-----------|\n| GnG | G&G |\n"
  },
  {
    "path": "docs/templates/ImageRef.md",
    "content": "# ImageRef\n\nCreate links to referenced images.\n\nThe general form of a markdown image link is: `![alt text](vaultPath \"title\")`.\nYou can also use anchors to position the image within the page,\nwhich creates links that look like this: `![alt text](vaultPath#anchor \"title\")`.\n\n## Anchor Tags\n\nAnchor tags are used to position images within a page and are styled with CSS. Examples:\n\n- `center` centers the image and constrains its height.\n- `gallery` constrains images within a gallery callout.\n- `portrait` floats an image to the right.\n- `symbol` floats Deity symbols to the right.\n- `token` is a smaller image, also floated to the right. Used in statblocks.\n\n## Attributes\n\n[embeddedLink](#embeddedlink), [shortTitle](#shorttitle), [title](#title), [vaultPath](#vaultpath)\n\n### embeddedLink\n\nReturn an embedded markdown link to the image, using an optional\nanchor tag to position the image in the page.\nFor example: `{resource.image.getEmbeddedLink(\"symbol\")}`\n\nIf the title is longer than 50 characters:\n`![{resource.shortTitle}]({resource.vaultPath}#anchor \"{resource.title}\")`,\n\nIf the title is 50 characters or less:\n`![{resource.title}]({resource.vaultPath}#anchor)`,\n\nLinks will be generated using \"center\" as the anchor by default.\n\n### shortTitle\n\nA shortened image title (max 50 characters) for use in markdown links.\n\n### title\n\nDescriptive title (or caption) for the image. This can be long.\n\n### vaultPath\n\nPath of the image in the vault or url for external images.\n"
  },
  {
    "path": "docs/templates/NamedText.md",
    "content": "# NamedText\n\nHolder of a name or category and associated descriptive text.\n\nThis attribute will render itself as labeled elements if you reference it directly.\n\n## Attributes\n\n[category](#category), [desc](#desc), [key](#key), [name](#name), [nested](#nested), [text](#text), [value](#value)\n\n### category\n\nAlternate accessor for the name\n\n### desc\n\nPre-formatted text description including all nested items\n\n### key\n\nAlternate accessor for the name\n\n### name\n\nName\n\n### nested\n\nList of child elements (mostly for YAML)\n\n### text\n\nAlternate accessor for formatted/descriptive text\n\n### value\n\nAlternate accessor for formatted/descriptive text\n"
  },
  {
    "path": "docs/templates/QuteAltNames.md",
    "content": "# QuteAltNames\n\n\n## Attributes\n\n[altNames](#altnames)\n\n### altNames\n\nAlternate names. (optional)\n"
  },
  {
    "path": "docs/templates/QuteBase.md",
    "content": "# QuteBase\n\nDefines attributes inherited by other Qute templates.\n\nNotes created from `QuteBase` (or a derivative) will use a specific template\nfor the type. For example, `QuteBackground` will use `background2md.txt`.\n\n## Attributes\n\n[books](#books), [getAliases](#getaliases), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath)\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### name\n\nNote name\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](Reprinted.md))\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/QuteNote.md",
    "content": "# QuteNote\n\nCommon attributes for simple notes. THese attributes are more\noften used by books, adventures, rules, etc.\n\nNotes created from `QuteNote` (or a derivative) will look for a template\nnamed `note2md.txt` by default.\n\n## Attributes\n\n[books](#books), [getAliases](#getaliases), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath)\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### name\n\nNote name\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](Reprinted.md))\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/README.md",
    "content": "# Qute Template Reference\n\nThe following pages describe attributes that you can use to customize\ngenerated output in Qute templates.\n\nUse a `resource.` prefix to access these attributes unless otherwise noted.\nFor example, `resource.title`.\n\nFor more information about Qute, see the [Qute guide](https://quarkus.io/guides/qute).\n\n- [Documentation for using templates with the CLI](../../examples/templates/README.md)\n- [5e Template Examples](../../examples/templates/tools5e/README.md)\n- [5eTools template attributes](dnd5e/README.md)\n- [Pf2eTools template attributes](pf2e/README.md)\n\n## References\n\n- [ImageRef](ImageRef.md): Create links to referenced images.\n- [NamedText](NamedText.md): Holder of a name or category and associated descriptive text.\n- [QuteBase](QuteBase.md): Defines attributes inherited by other Qute templates.\n- [QuteNote](QuteNote.md): Common attributes for simple notes.\n- [Reprinted](Reprinted.md): A simple record to hold the name and source of a reprinted item.\n- [SourceAndPage](SourceAndPage.md): A representation of a source and page number.\n- [TtrpgTemplateExtension](TtrpgTemplateExtension.md): Qute template extensions for TTRPG data.\n"
  },
  {
    "path": "docs/templates/Reprinted.md",
    "content": "# Reprinted\n\nA simple record to hold the name and source of a reprinted item.\n\n## Attributes\n\n[name](#name), [source](#source)\n\n### name\n\nName of the reprinted item\n\n### source\n\nPrimary source of the reprinted item\n"
  },
  {
    "path": "docs/templates/SourceAndPage.md",
    "content": "# SourceAndPage\n\nA representation of a source and page number. This attribute will print\nitself nicely if you don't reference sub-attributes.\n\n## Attributes\n\n[longName](#longname), [page](#page), [source](#source)\n\n### longName\n\nLong source name\n\n### page\n\nAssociated page number (may not be present)\n\n### source\n\nAbbreviated source name\n"
  },
  {
    "path": "docs/templates/TtrpgTemplateExtension.md",
    "content": "# TtrpgTemplateExtension\n\nQute template extensions for TTRPG data.\n\nUse these functions to help render TTRPG data in Qute templates.\n\n## Attributes\n\n[asBonus](#asbonus), [capitalized](#capitalized), [capitalizedList](#capitalizedlist), [first](#first), [join](#join), [joinConjunct](#joinconjunct), [jsonString](#jsonstring), [lowercase](#lowercase), [pluralizeLabel](#pluralizelabel), [prefixSpace](#prefixspace), [quotedEscaped](#quotedescaped), [size](#size), [skipFirst](#skipfirst), [uppercaseFirst](#uppercasefirst)\n\n### asBonus\n\nReturn the value formatted with a bonus with a +/- prefix.\n\nUsage: `{perception.asBonus}`\n\n### capitalized\n\nReturn a Title Case form of this string, capitalizing the first word.\nDoes not transform the contents of parenthesis (like markdown URLs).\n\nUsage: `{resource.languages.capitalized}`\n\n### capitalizedList\n\nReturn a capitalized form of this string, capitalizing the first word of each clause.\nClauses are separated by commas or semicolons. Ignores conjunctions and parenthetical content.\n\nUsage: `{resource.languages.capitalizedList}`\n\n### first\n\nFirst element in list\n\nUsage: `{resource.components.first}`\n\n### join\n\nReturn the given collection converted into a string and joined using the specified joiner.\n\nUsage: `{resource.components.join(\", \")}`\n\n### joinConjunct\n\nReturn the given list joined into a single string, using a different delimiter for the last element.\n\nUsage: `{resource.components.joinConjunct(\", \", \" or \")}`\n\n### jsonString\n\nReturn the object as a JSON string\n\nUsage: `{resource.components.getJsonString(resource)}`\n\n### lowercase\n\nReturn the lowercase form of this string.\nDoes not transform the contents of parenthesis (like markdown URLs).\n\nUsage: `{resource.name.lowercase}`\n\n### pluralizeLabel\n\nReturn the string pluralized based on the size of the collection.\n\nUsage: `{resource.name.pluralized(resource.components)}`\n\n### prefixSpace\n\nReturn the given object as a string, with a space prepended if it's non-empty and non-null.\n\nUsage: `{resource.name.prefixSpace}`\n\n### quotedEscaped\n\nEscape double quotes in a string (YAML/properties safe)\n\nUsage: `{resource.components.quotedEscaped}`\n\n### size\n\nReturn the size of a list\n\nUsage: `{resource.components.size()}`\n\n### skipFirst\n\nSkip first element in list\n\nUsage: `{resource.components.skipFirst}`\n\n### uppercaseFirst\n\nReturn the text with a capitalized first letter (ignoring punctuation like '[')\n\nUsage: `{resource.name.uppercaseFirst}`\n"
  },
  {
    "path": "docs/templates/dnd5e/AbilityScores/AbilityScore.md",
    "content": "# AbilityScore\n\nAbility score. Usually an integer, but can be a special value (string) instead.\n\n## Attributes\n\n[score](#score), [special](#special)\n\n### score\n\nThe ability score (integer).\n\n### special\n\nThe special value (string), or null if not applicable.\n"
  },
  {
    "path": "docs/templates/dnd5e/AbilityScores/README.md",
    "content": "# AbilityScores\n\n5eTools Ability Score attributes.\n\nUsed to describe a monster, object or vehicle's ability scores.\n\nIf referenced as a unit (ignoring inner attributes), it will render ability scores as\na `|` separated list of values, in `STR,DEX,CON,INT,WIS,CHA` order.\n\nFor example:\n`10 (+0)|10 (+0)|10 (+0)|10 (+0)|10 (+0)|10 (+0)`.\n\n## Attributes\n\n[cha](#cha), [chaMod](#chamod), [chaStat](#chastat), [charisma](#charisma), [con](#con), [conMod](#conmod), [conStat](#constat), [constitution](#constitution), [dex](#dex), [dexMod](#dexmod), [dexStat](#dexstat), [dexterity](#dexterity), [int](#int), [intMod](#intmod), [intStat](#intstat), [intelligence](#intelligence), [score](#score), [str](#str), [strMod](#strmod), [strStat](#strstat), [strength](#strength), [wis](#wis), [wisMod](#wismod), [wisStat](#wisstat), [wisdom](#wisdom)\n\n### cha\n\nCharisma as an ability string: `10 (+0)`\n\n### chaMod\n\nCharisma modifier: +1 or -2\n\n### chaStat\n\nCharisma stat as a number: 10\n\n### charisma\n\nCharisma score as [AbilityScore](AbilityScore.md)\n\n### con\n\nConstitution as an ability string: `10 (+0)`\n\n### conMod\n\nConstitution modifier: +1 or -2\n\n### conStat\n\nConstitution score as a number: 10\n\n### constitution\n\nConstitution score as [AbilityScore](AbilityScore.md)\n\n### dex\n\nDexterity as an ability string: `10 (+0)`\n\n### dexMod\n\nDexterity modifier: +1 or -2\n\n### dexStat\n\nDexterity score as a number: 10\n\n### dexterity\n\nDexterity score as [AbilityScore](AbilityScore.md)\n\n### int\n\nIntelligence as an ability string: `10 (+0)`\n\n### intMod\n\nIntelligence modifier: +1 or -2\n\n### intStat\n\nIntelligence score as a number: 10\n\n### intelligence\n\nIntelligence score as [AbilityScore](AbilityScore.md)\n\n### score\n\n\n### str\n\nStrength as an ability string: `10 (+0)`\n\n### strMod\n\nStrength modifier: +1 or -2\n\n### strStat\n\nStrength score as a number: 10\n\n### strength\n\nStrength score as [AbilityScore](AbilityScore.md)\n\n### wis\n\nWisdom as an ability string: `10 (+0)`\n\n### wisMod\n\nWisdom modifier: +1 or -2\n\n### wisStat\n\nWisdom score as a number: 10\n\n### wisdom\n\nWisdom score as [AbilityScore](AbilityScore.md)\n"
  },
  {
    "path": "docs/templates/dnd5e/AbilityScores.md",
    "content": "# AbilityScores\n\n5eTools Ability Score attributes.\n\nUsed to describe a monster, object or vehicle's ability scores.\n\nIf referenced as a unit (ignoring inner attributes), it will render ability scores as\na `|` separated list of values, in `STR,DEX,CON,INT,WIS,CHA` order.\n\nFor example:\n`10 (+0)|10 (+0)|10 (+0)|10 (+0)|10 (+0)|10 (+0)`.\n\n## Attributes\n\n[cha](#cha), [chaMod](#chamod), [chaStat](#chastat), [con](#con), [conMod](#conmod), [conStat](#constat), [dex](#dex), [dexMod](#dexmod), [dexStat](#dexstat), [int](#int), [intMod](#intmod), [intStat](#intstat), [str](#str), [strMod](#strmod), [strStat](#strstat), [wis](#wis), [wisMod](#wismod), [wisStat](#wisstat)\n\n### cha\n\nCharisma as an ability string: `10 (+0)`\n\n### chaMod\n\nCharisma modifier: +1 or -2\n\n### chaStat\n\nCharisma stat as a number: 10\n\n### con\n\nConstitution as an ability string: `10 (+0)`\n\n### conMod\n\nConstitution modifier: +1 or -2\n\n### conStat\n\nConstitution score as a number: 10\n\n### dex\n\nDexterity as an ability string: `10 (+0)`\n\n### dexMod\n\nDexterity modifier: +1 or -2\n\n### dexStat\n\nDexterity score as a number: 10\n\n### int\n\nIntelligence as an ability string: `10 (+0)`\n\n### intMod\n\nIntelligence modifier: +1 or -2\n\n### intStat\n\nIntelligence score as a number: 10\n\n### str\n\nStrength as an ability string: `10 (+0)`\n\n### strMod\n\nStrength modifier: +1 or -2\n\n### strStat\n\nStrength score as a number: 10\n\n### wis\n\nWisdom as an ability string: `10 (+0)`\n\n### wisMod\n\nWisdom modifier: +1 or -2\n\n### wisStat\n\nWisdom score as a number: 10\n"
  },
  {
    "path": "docs/templates/dnd5e/AcHp.md",
    "content": "# AcHp\n\n5eTools armor class and hit points attributes\n\nThis data object provides a default mechanism for creating\na marked up string based on the attributes that are present.\nTo use it, reference it directly.\n\n## Attributes\n\n[ac](#ac), [acText](#actext), [hitDice](#hitdice), [hp](#hp), [hpDiceRoller](#hpdiceroller), [hpText](#hptext)\n\n### ac\n\nArmor class (number)\n\n### acText\n\nAdditional armor class text. May link to related items\n\n### hitDice\n\nHit dice formula string: 7d10 + 14 (for creatures)\n\n### hp\n\nHit points (number or —)\n\n### hpDiceRoller\n\nHit points as a dice roller formula:\n\\`dice: 1d20+7|text(37)\\` (\\`1d20+7\\`)\n\n### hpText\n\nAdditional hit point text.\nIn the case of summoned creatures, this will contain notes for how hit points\nshould be calculated relative to the player's modifiers.\n"
  },
  {
    "path": "docs/templates/dnd5e/ImmuneResist.md",
    "content": "# ImmuneResist\n\n5eTools vulnerabilities, resistances, immunities, and condition immunities\n\nThis data object provides a default mechanism for creating\na marked up string based on the attributes that are present.\nTo use it, reference it directly.\n\n## Attributes\n\n[conditionImmune](#conditionimmune), [immune](#immune), [present](#present), [resist](#resist), [vulnerable](#vulnerable)\n\n### conditionImmune\n\nComma-separated string of creature condition immunities (if present).\n\n### immune\n\nComma-separated string of creature damage immunities (if present).\n\n### present\n\nTrue if immunities or resistances are present (otherwise false)\n\n### resist\n\nComma-separated string of creature damage resistances (if present).\n\n### vulnerable\n\nComma-separated string of creature damage vulnerabilities (if present).\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteBackground.md",
    "content": "# QuteBackground\n\n5eTools background attributes (`background2md.txt`).\n\nExtension of [Tools5eQuteBase](Tools5eQuteBase.md).\n\n## Attributes\n\n[ability](#ability), [books](#books), [fluffImages](#fluffimages), [getAliases](#getaliases), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [prerequisite](#prerequisite), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath)\n\n### ability\n\nFormatted text listing ability score increase (optional)\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### fluffImages\n\nList of images as [ImageRef](../ImageRef.md) (optional)\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasImages\n\nReturn true if any images are present\n\n### hasMoreImages\n\nReturn true if more than one image is present\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### name\n\nNote name\n\n### prerequisite\n\nFormatted text listing other prerequisite conditions (optional)\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../Reprinted.md))\n\n### showAllImages\n\nReturn embedded wikilinks for all images\nIf there is more than one, they will be displayed in a gallery.\n\n### showMoreImages\n\nReturn embedded wikilinks for all but the first image\nIf there is more than one, they will be displayed in a gallery.\n\n### showPortraitImage\n\nReturn an embedded wikilink to the first image\nWill have the \"right\" anchor tag.\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteBastion/Hireling.md",
    "content": "# Hireling\n\nHireling information. Either exact or min must be present.\n\n## Attributes\n\n[description](#description), [exact](#exact), [max](#max), [min](#min), [space](#space)\n\n### description\n\nFormatted string description of the hirelings for a Bastion\n\n### exact\n\nExact number of hirelings (either exact or min)\n\n### max\n\nMaximum number of hirelings (optional)\n\n### min\n\nMinimum number of hirelings (either exact or min)\n\n### space\n\nSize of bastion space required for these hirelings (optional)\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteBastion/README.md",
    "content": "# QuteBastion\n\n5eTools background attributes (`bastion2md.txt`).\n\nExtension of [Tools5eQuteBase](../Tools5eQuteBase.md).\n\n## Attributes\n\n[books](#books), [fluffImages](#fluffimages), [getAliases](#getaliases), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [hirelingDescription](#hirelingdescription), [hirelings](#hirelings), [labeledSource](#labeledsource), [level](#level), [name](#name), [orders](#orders), [prerequisite](#prerequisite), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [space](#space), [spaceDescription](#spacedescription), [tags](#tags), [text](#text), [type](#type), [vaultPath](#vaultpath)\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### fluffImages\n\nList of images as [ImageRef](../../ImageRef.md) (optional)\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasImages\n\nReturn true if any images are present\n\n### hasMoreImages\n\nReturn true if more than one image is present\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### hirelingDescription\n\nHirelings as a descriptive string (if hirelings is present)\n\n### hirelings\n\nList of possible hirelings this bastion can have (as [Hireling](Hireling.md),\noptional)\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### level\n\nBastion level (optional)\n\n### name\n\nNote name\n\n### orders\n\nBastion orders (optional)\n\n### prerequisite\n\nFormatted text listing other prerequisite conditions (optional)\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../../Reprinted.md))\n\n### showAllImages\n\nReturn embedded wikilinks for all images\nIf there is more than one, they will be displayed in a gallery.\n\n### showMoreImages\n\nReturn embedded wikilinks for all but the first image\nIf there is more than one, they will be displayed in a gallery.\n\n### showPortraitImage\n\nReturn an embedded wikilink to the first image\nWill have the \"right\" anchor tag.\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### space\n\nList of possible spaces this bastion can occupy (as [Space](Space.md),\noptional)\n\n### spaceDescription\n\nSpace as a descriptive string (if space is present)\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### type\n\nType\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteBastion/Space.md",
    "content": "# Space\n\n\n## Attributes\n\n[cost](#cost), [description](#description), [name](#name), [prevSpace](#prevspace), [squares](#squares), [time](#time)\n\n### cost\n\nCost (GP) of building a bastion of this size\n\n### description\n\nFormatted string description of the space required for (or occupied by) a Bastion\n\n### name\n\nName of this size/space\n\n### prevSpace\n\nPrevious space to enlarge from (optional)\n\n### squares\n\nMaximum number of 5-foot squares a bastion this size can occupy\n\n### time\n\nTime to construct a bastion of this size\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteClass/HitPointDie.md",
    "content": "# HitPointDie\n\nDescribes the hit point die used by the class.\n\nIf referenced as a unit (ignoring inner attributes), it will render\nformatted strings based on the class version (2024 or not).\n\n## Attributes\n\n[average](#average), [classic](#classic), [face](#face), [isClassic](#isclassic), [isSidekick](#issidekick), [name](#name), [number](#number), [sidekick](#sidekick)\n\n### average\n\nThe average value of a hit dice roll\n\n### classic\n\n\n### face\n\nDie to roll (8, 10); This will be 0 for sidekicks\n\n### isClassic\n\nTrue if this is a 2014 class\n\n### isSidekick\n\nExplicit test for sidekick (alternate to 0 face)\n\n### name\n\n\n### number\n\nHow many dice to roll (pretty much always 1)\n\n### sidekick\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteClass/Multiclassing.md",
    "content": "# Multiclassing\n\nDescribes the multiclassing information for the class.\n\nIf referenced as a unit (ignoring inner attributes), it will render\nformatted text describing multiclassing requirements and proficiencies.\n\n## Attributes\n\n[armor](#armor), [classic](#classic), [isClassic](#isclassic), [primaryAbility](#primaryability), [requirements](#requirements), [requirementsSpecial](#requirementsspecial), [skills](#skills), [text](#text), [tools](#tools), [weapons](#weapons)\n\n### armor\n\nArmor proficiencies gained as formatted string\n(optional)\n\n### classic\n\n\n### isClassic\n\nTrue if this class is from the 2014 edition\n\n### primaryAbility\n\nPrimary ability for multiclassing as formatted\nstring (optional)\n\n### requirements\n\nPrerequisites for multiclassing as formatted\nstring (optional)\n\n### requirementsSpecial\n\nSpecial prerequisites for multiclassing as\nformatted string (optional)\n\n### skills\n\nSkill proficiencies gained as formatted string\n(optional)\n\n### text\n\nFormatted text describing this multiclass\n(optional)\n\n### tools\n\nTool proficiencies gained as formatted string\n(optional)\n\n### weapons\n\nWeapon proficiencies gained as formatted string\n(optional)\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteClass/README.md",
    "content": "# QuteClass\n\n5eTools class attributes (`class2md.txt`)\n\nExtension of [Tools5eQuteBase](../Tools5eQuteBase.md).\n\n## Attributes\n\n[books](#books), [classProgression](#classprogression), [fluffImages](#fluffimages), [getAliases](#getaliases), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [hitDice](#hitdice), [hitPointDie](#hitpointdie), [hitRollAverage](#hitrollaverage), [labeledSource](#labeledsource), [multiclassing](#multiclassing), [name](#name), [primaryAbility](#primaryability), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [startingEquipment](#startingequipment), [tags](#tags), [text](#text), [vaultPath](#vaultpath)\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### classProgression\n\nFormatted callout containing class and feature progressions.\n\n### fluffImages\n\nList of images as [ImageRef](../../ImageRef.md) (optional)\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasImages\n\nReturn true if any images are present\n\n### hasMoreImages\n\nReturn true if more than one image is present\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### hitDice\n\nHit dice for this class as a single digit: 8\n\n### hitPointDie\n\nHit point die for this class as\n[HitPointDie](HitPointDie.md)\n\n### hitRollAverage\n\nAverage Hit dice roll as a single digit\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### multiclassing\n\nMulticlassing requirements and proficiencies for this class as\n[Multiclassing](Multiclassing.md)\n\n### name\n\nNote name\n\n### primaryAbility\n\nFormatted string describing the primary abilities for this class\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../../Reprinted.md))\n\n### showAllImages\n\nReturn embedded wikilinks for all images\nIf there is more than one, they will be displayed in a gallery.\n\n### showMoreImages\n\nReturn embedded wikilinks for all but the first image\nIf there is more than one, they will be displayed in a gallery.\n\n### showPortraitImage\n\nReturn an embedded wikilink to the first image\nWill have the \"right\" anchor tag.\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### startingEquipment\n\nFormatted text describing starting equipment as\n[StartingEquipment](StartingEquipment.md)\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteClass/StartingEquipment.md",
    "content": "# StartingEquipment\n\nDescribes the starting equipment for the class.\n\nIf referenced as a unit (ignoring inner attributes), it will render\nstructured text describing starting proficiencies and equipment *2014* vs\n*2024*.\n\n## Attributes\n\n[armor](#armor), [armorString](#armorstring), [classic](#classic), [equipment](#equipment), [isClassic](#isclassic), [joinOrDefault](#joinordefault), [proficiencies](#proficiencies), [savingThrows](#savingthrows), [skills](#skills), [tools](#tools), [weapons](#weapons)\n\n### armor\n\nList of armor as formatted strings (links)\n\n### armorString\n\nCreate a structured string describing armor training.\nSlighly different formatting and joining for 2014 vs 2024 materials.\n\n### classic\n\n\n### equipment\n\nList of equipment as formatted strings (links)\n\n### isClassic\n\nTrue if this class is from the 2014 edition\n\n### joinOrDefault\n\nGiven a list of strings, return a formatted string with a conjunction.\n\n### proficiencies\n\nFormatted string of class proficiencies\n\n### savingThrows\n\nList of saving throws\n\n### skills\n\nList of skills as formatted strings (links)\n\n### tools\n\nList of tools as formatted strings (links)\n\n### weapons\n\nList of weapons as formatted strings (links)\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteClass.md",
    "content": "# QuteClass\n\n5eTools class attributes (`class2md.txt`)\n\nExtension of [Tools5eQuteBase](Tools5eQuteBase.md).\n\n## Attributes\n\n[classProgression](#classprogression), [hasSections](#hassections), [hitDice](#hitdice), [hitRollAverage](#hitrollaverage), [labeledSource](#labeledsource), [multiclassing](#multiclassing), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [startingEquipment](#startingequipment), [tags](#tags), [text](#text), [vaultPath](#vaultpath)\n\n\n### classProgression\n\nFormatted callout containing class and feature progressions.\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### hitDice\n\nHit dice for this class as a single digit: 8\n\n### hitRollAverage\n\nThe average roll for a hit die of this class, for example: `add {resource.hitRollAverage}...`\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### multiclassing\n\nFormatted text section describing how to multiclass with this class\n\n### name\n\nNote name\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../Reprinted.md))\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../SourceAndPage.md)\n\n### startingEquipment\n\nFormatted text describing starting equipment\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteDeck/Card.md",
    "content": "# Card\n\n\n## Attributes\n\n[face](#face), [name](#name), [sourceAndPage](#sourceandpage), [suitValue](#suitvalue), [text](#text)\n\n### face\n\nImage from the front of the card as [ImageRef](../../ImageRef.md) (optional)\n\n### name\n\nName of the card\n\n### sourceAndPage\n\nSource and page containing card definition as [SourceAndPage](../../SourceAndPage.md)\n\n### suitValue\n\nCard suit and value (optional)\n\n### text\n\nText on the front of the card\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteDeck/README.md",
    "content": "# QuteDeck\n\n5eTools deck attributes (`deck2md.txt`)\n\nExtension of [Tools5eQuteBase](../Tools5eQuteBase.md).\n\n## Attributes\n\n[books](#books), [cardBack](#cardback), [cards](#cards), [fluffImages](#fluffimages), [getAliases](#getaliases), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath)\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### cardBack\n\nImage from the back of the card as [ImageRef](../../ImageRef.md) (optional)\n\n### cards\n\nList of cards in the deck\n\n### fluffImages\n\nList of images as [ImageRef](../../ImageRef.md) (optional)\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasImages\n\nReturn true if any images are present\n\n### hasMoreImages\n\nReturn true if more than one image is present\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### name\n\nNote name\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../../Reprinted.md))\n\n### showAllImages\n\nReturn embedded wikilinks for all images\nIf there is more than one, they will be displayed in a gallery.\n\n### showMoreImages\n\nReturn embedded wikilinks for all but the first image\nIf there is more than one, they will be displayed in a gallery.\n\n### showPortraitImage\n\nReturn an embedded wikilink to the first image\nWill have the \"right\" anchor tag.\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteDeity.md",
    "content": "# QuteDeity\n\n5eTools deity attributes (`deity2md.txt`)\n\nExtension of [Tools5eQuteBase](Tools5eQuteBase.md).\n\n## Attributes\n\n[alignment](#alignment), [altNames](#altnames), [books](#books), [category](#category), [domains](#domains), [fluffImages](#fluffimages), [getAliases](#getaliases), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [image](#image), [labeledSource](#labeledsource), [name](#name), [pantheon](#pantheon), [province](#province), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [symbol](#symbol), [tags](#tags), [text](#text), [title](#title), [vaultPath](#vaultpath)\n\n### alignment\n\nAlignment of this deity\n\n### altNames\n\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### category\n\nCategory of this deity: Lesser Idols, Prime Deities\n\n### domains\n\nCategory of this deity: Nature, Tempest\n\n### fluffImages\n\nList of images as [ImageRef](../ImageRef.md) (optional)\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasImages\n\nReturn true if any images are present\n\n### hasMoreImages\n\nReturn true if more than one image is present\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### image\n\nImage or symbol representing this deity (as [ImageRef](../ImageRef.md))\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### name\n\nNote name\n\n### pantheon\n\nPantheon to which this deity belongs: Celtic\n\n### province\n\nProvince of this deity: Discovery, Luck, Storms, Travel, ...\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../Reprinted.md))\n\n### showAllImages\n\nReturn embedded wikilinks for all images\nIf there is more than one, they will be displayed in a gallery.\n\n### showMoreImages\n\nReturn embedded wikilinks for all but the first image\nIf there is more than one, they will be displayed in a gallery.\n\n### showPortraitImage\n\nReturn an embedded wikilink to the first image\nWill have the \"right\" anchor tag.\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### symbol\n\nText description of deity's symbol: Wave of white water on green\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### title\n\nTitle of this deity\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteFeat.md",
    "content": "# QuteFeat\n\n5eTools feat and optional feat attributes (`feat2md.txt`)\n\nExtension of [Tools5eQuteBase](Tools5eQuteBase.md).\n\n## Attributes\n\n[ability](#ability), [books](#books), [category](#category), [fluffImages](#fluffimages), [getAliases](#getaliases), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [level](#level), [name](#name), [prerequisite](#prerequisite), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath)\n\n### ability\n\nFormatted text listing ability score increase (optional)\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### category\n\nFeat category (optional)\n\n### fluffImages\n\nList of images as [ImageRef](../ImageRef.md) (optional)\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasImages\n\nReturn true if any images are present\n\n### hasMoreImages\n\nReturn true if more than one image is present\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### level\n\nPrerequisite level\n\n### name\n\nNote name\n\n### prerequisite\n\nFormatted text listing other prerequisite conditions (optional)\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../Reprinted.md))\n\n### showAllImages\n\nReturn embedded wikilinks for all images\nIf there is more than one, they will be displayed in a gallery.\n\n### showMoreImages\n\nReturn embedded wikilinks for all but the first image\nIf there is more than one, they will be displayed in a gallery.\n\n### showPortraitImage\n\nReturn an embedded wikilink to the first image\nWill have the \"right\" anchor tag.\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteHazard.md",
    "content": "# QuteHazard\n\n5eTools hazard attributes (`hazard2md.txt`)\n\nExtension of [Tools5eQuteBase](Tools5eQuteBase.md).\n\n## Attributes\n\n[books](#books), [fluffImages](#fluffimages), [getAliases](#getaliases), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [hazardType](#hazardtype), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath)\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### fluffImages\n\nList of images as [ImageRef](../ImageRef.md) (optional)\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasImages\n\nReturn true if any images are present\n\n### hasMoreImages\n\nReturn true if more than one image is present\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### hazardType\n\nType of hazard: \"Magical Trap\", \"Wilderness Hazard\"\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### name\n\nNote name\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../Reprinted.md))\n\n### showAllImages\n\nReturn embedded wikilinks for all images\nIf there is more than one, they will be displayed in a gallery.\n\n### showMoreImages\n\nReturn embedded wikilinks for all but the first image\nIf there is more than one, they will be displayed in a gallery.\n\n### showPortraitImage\n\nReturn an embedded wikilink to the first image\nWill have the \"right\" anchor tag.\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteItem/README.md",
    "content": "# QuteItem\n\n5eTools item attributes (`item2md.txt`)\n\nExtension of [Tools5eQuteBase](../Tools5eQuteBase.md).\n\n## Attributes\n\n[altNames](#altnames), [armorClass](#armorclass), [books](#books), [cost](#cost), [costCp](#costcp), [damage](#damage), [damage2h](#damage2h), [detail](#detail), [fluffImages](#fluffimages), [getAliases](#getaliases), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [mastery](#mastery), [name](#name), [prerequisite](#prerequisite), [properties](#properties), [range](#range), [reprintOf](#reprintof), [rootVariant](#rootvariant), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [stealthPenalty](#stealthpenalty), [strengthRequirement](#strengthrequirement), [subtypeString](#subtypestring), [tags](#tags), [text](#text), [variantAliases](#variantaliases), [variantSectionLinks](#variantsectionlinks), [variants](#variants), [vaultPath](#vaultpath), [weight](#weight)\n\n### altNames\n\n\n### armorClass\n\nChanges to armor class provided by the item, if applicable\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### cost\n\nCost of the item (gp, sp, cp). Optional.\n\n### costCp\n\nCost of the item (cp) as number. Optional.\n\n### damage\n\nOne-handed Damage string, if applicable. Contains dice formula and damage type\n\n### damage2h\n\nTwo-handed Damage string, if applicable. Contains dice formula and damage type\n\n### detail\n\nFormatted string of item details. Will include some combination of tier, rarity, category, and attunement\n\n### fluffImages\n\nList of images as [ImageRef](../../ImageRef.md) (optional)\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasImages\n\nReturn true if any images are present\n\n### hasMoreImages\n\nReturn true if more than one image is present\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### mastery\n\nFormatted string listing applicable item mastery (with links to rules if the source is present)\n\n### name\n\nNote name\n\n### prerequisite\n\nFormatted text listing other prerequisite conditions (optional)\n\n### properties\n\nFormatted string listing item's properties (with links to rules if the source is present)\n\n### range\n\nItem's range, if applicable\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../../Reprinted.md))\n\n### rootVariant\n\nDetailed information about this item as [Variant](Variant.md)\n\n### showAllImages\n\nReturn embedded wikilinks for all images\nIf there is more than one, they will be displayed in a gallery.\n\n### showMoreImages\n\nReturn embedded wikilinks for all but the first image\nIf there is more than one, they will be displayed in a gallery.\n\n### showPortraitImage\n\nReturn an embedded wikilink to the first image\nWill have the \"right\" anchor tag.\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### stealthPenalty\n\nTrue if the item imposes a stealth penalty, if applicable\n\n### strengthRequirement\n\nStrength requirement as a numerical value, if applicable\n\n### subtypeString\n\nFormatted string of additional item attributes. Optional.\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### variantAliases\n\nString: list (`- \"alias\"`) of aliases for variants. Use in YAML frontmatter with `aliases:`.\nWill return an empty string if there are no variants\n\n### variantSectionLinks\n\nString: list (`- [name](#anchor)`) of links to variant sections.\nWill return an empty string if there are no variants.\n\n### variants\n\nList of magic item variants as [Variant](Variant.md). Optional.\n\n### vaultPath\n\nPath to this note in the vault\n\n### weight\n\nWeight of the item (pounds) as a decimal value\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteItem/Variant.md",
    "content": "# Variant\n\n\n## Attributes\n\n[age](#age), [ammo](#ammo), [armorClass](#armorclass), [attunement](#attunement), [baseItem](#baseitem), [bonusAbilityCheck](#bonusabilitycheck), [bonusAc](#bonusac), [bonusProficiencyBonus](#bonusproficiencybonus), [bonusSavingThrow](#bonussavingthrow), [bonusSavingThrowConcentration](#bonussavingthrowconcentration), [bonusSpellAttack](#bonusspellattack), [bonusSpellDamage](#bonusspelldamage), [bonusSpellSaveDc](#bonusspellsavedc), [bonusWeapon](#bonusweapon), [bonusWeaponAttack](#bonusweaponattack), [bonusWeaponCritDamage](#bonusweaponcritdamage), [bonusWeaponDamage](#bonusweapondamage), [cost](#cost), [costCp](#costcp), [cursed](#cursed), [damage](#damage), [damage2h](#damage2h), [detail](#detail), [firearm](#firearm), [focus](#focus), [focusType](#focustype), [mastery](#mastery), [masteryList](#masterylist), [name](#name), [poison](#poison), [poisonTypes](#poisontypes), [prerequisite](#prerequisite), [properties](#properties), [propertiesList](#propertieslist), [range](#range), [rarity](#rarity), [staff](#staff), [stealthPenalty](#stealthpenalty), [strengthRequirement](#strengthrequirement), [subtypeString](#subtypestring), [tattoo](#tattoo), [tier](#tier), [type](#type), [typeAlt](#typealt), [weaponCategory](#weaponcategory), [weight](#weight), [wondrous](#wondrous)\n\n### age\n\nAge/Era of item. Optional. Known values: futuristic, industrial, modern, renaissance, victorian.\n\n### ammo\n\nTrue if this is ammunition\n\n### armorClass\n\nChanges to armor class provided by the item. Optional.\n\n### attunement\n\nAttunement requirements. Optional. One of: required, optional, prerequisites/conditions (implies\nrequired).\n\n### baseItem\n\nMarkdown link to base item. Optional.\n\n### bonusAbilityCheck\n\nBonus to ability check rolls provided by the item. Optional.\n\n### bonusAc\n\nBonus to armor class provided by the item. Optional.\n\n### bonusProficiencyBonus\n\nBonus to proficiency bonus provided by the item. Optional.\n\n### bonusSavingThrow\n\nBonus to saving throw rolls provided by the item. Optional.\n\n### bonusSavingThrowConcentration\n\nBonus to concentration saving throw rolls provided by the item. Optional.\n\n### bonusSpellAttack\n\nBonus to spell attack rolls provided by the item. Optional.\n\n### bonusSpellDamage\n\nBonus to spell damage rolls provided by the item. Optional.\n\n### bonusSpellSaveDc\n\nBonus to spell save DC provided by the item. Optional.\n\n### bonusWeapon\n\nBonus to weapon attack and damage rolls provided by the item. Optional.\n\n### bonusWeaponAttack\n\nBonus to weapon attack rolls provided by the item. Optional.\n\n### bonusWeaponCritDamage\n\nBonus to weapon critical damage rolls provided by the item. Optional.\n\n### bonusWeaponDamage\n\nBonus to weapon damage rolls provided by the item. Optional.\n\n### cost\n\nCost of the item (gp, sp, cp). Usually missing for magic items.\n\n### costCp\n\nCost of the item (cp) as number. Usually missing for magic items.\n\n### cursed\n\nTrue if this is a cursed item\n\n### damage\n\nOne-handed Damage string. Contains dice formula and damage type. Optional.\n\n### damage2h\n\nTwo-handed Damage string. Contains dice formula and damage type. Optional.\n\n### detail\n\nFormatted string of item details. Will include some combination of tier, rarity, category, and attunement\n\n### firearm\n\nTrue if this is a firearm\n\n### focus\n\nTrue if this is a spellcasting focus.\n\n### focusType\n\nSpellcasting focus type. Optional. One of: \"arcane\", \"druid\", \"holy\", and/or a list of required classes.\n\n### mastery\n\nFormatted string listing applicable item mastery (with links to rules if the source is present)\n\n### masteryList\n\nList of item mastery that apply to this item.\n\n### name\n\nName of the variant.\n\n### poison\n\nTrue if this is a poison.\n\n### poisonTypes\n\nPoison type(s). Optional.\n\n### prerequisite\n\nFormatted text listing other prerequisite conditions. Optional.\n\n### properties\n\nFormatted string listing item's properties (with links to rules if the source is present)\n\n### propertiesList\n\nList of item's properties (with links to rules if the source is present).\n\n### range\n\nItem's range. Optional.\n\n### rarity\n\nItem rarity. Optional. One of: \"none\": mundane items; \"unknown (magic)\": miscellaneous magical items;\n\"unknown\": miscellaneous mundane items; \"varies\": item groups or magic variants.\n\n### staff\n\nTrue if this is a staff\n\n### stealthPenalty\n\nTrue if the item imposes a stealth penalty. Optional.\n\n### strengthRequirement\n\nStrength requirement as a numerical value. Optional.\n\n### subtypeString\n\nItem subtype string. Optional.\n\n### tattoo\n\nTrue if this is a tattoo\n\n### tier\n\nItem tier. Optional. One of: \"minor\", \"major\".\n\n### type\n\nItem type\n\n### typeAlt\n\nAlternate item type. Optional.\n\n### weaponCategory\n\nWeapon category. Optional. One of: \"simple\", \"martial\".\n\n### weight\n\nWeight of the item (pounds) as a decimal value.\n\n### wondrous\n\nTrue if this is a wondrous item\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteMonster/Initiative.md",
    "content": "# Initiative\n\n5eTools creature initiative attributes.\n\n## Attributes\n\n[bonus](#bonus), [mode](#mode), [passive](#passive), [passiveInitiative](#passiveinitiative)\n\n### bonus\n\nInitiative modifier\n\n### mode\n\nInitiative mode: \"advantage\", \"disadvantage\", or \"none\"\n\n### passive\n\nPassive initiative value (number)\n\n### passiveInitiative\n\nString representation of passive initiative value\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteMonster/InitiativeMode.md",
    "content": "# InitiativeMode\n\nInitiative mode: \"advantage\", \"disadvantage\", or \"none\"\n\n## Attributes\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteMonster/README.md",
    "content": "# QuteMonster\n\n5eTools creature attributes (`monster2md.txt`)\n\nExtension of [Tools5eQuteBase](../Tools5eQuteBase.md).\n\n## Attributes\n\n[5eInitiativeYaml](#5einitiativeyaml), [5eInitiativeYamlNoSource](#5einitiativeyamlnosource), [5eStatblockYaml](#5estatblockyaml), [5eStatblockYamlNoSource](#5estatblockyamlnosource), [ac](#ac), [acHp](#achp), [acText](#actext), [action](#action), [alignment](#alignment), [allTraits](#alltraits), [bonusAction](#bonusaction), [books](#books), [conditionImmune](#conditionimmune), [cr](#cr), [description](#description), [environment](#environment), [fluffImages](#fluffimages), [fullType](#fulltype), [gear](#gear), [getAliases](#getaliases), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [hitDice](#hitdice), [hp](#hp), [hpText](#hptext), [immune](#immune), [immuneResist](#immuneresist), [initiative](#initiative), [isNpc](#isnpc), [labeledSource](#labeledsource), [languages](#languages), [legendary](#legendary), [legendaryGroup](#legendarygroup), [legendaryGroupLink](#legendarygrouplink), [name](#name), [passive](#passive), [pb](#pb), [rawSpellcasting](#rawspellcasting), [reaction](#reaction), [reprintOf](#reprintof), [resist](#resist), [savesSkills](#savesskills), [savingThrows](#savingthrows), [scores](#scores), [senses](#senses), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [size](#size), [skills](#skills), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [speed](#speed), [spellcasting](#spellcasting), [subtype](#subtype), [tags](#tags), [text](#text), [token](#token), [trait](#trait), [type](#type), [vaultPath](#vaultpath), [vulnerable](#vulnerable)\n\n### 5eInitiativeYaml\n\nA minimal YAML snippet containing monster attributes required by the\nInitiative Tracker plugin. Use this in frontmatter.\n\nThe source book will be included in the name if it isn't the default monster source (\"MM\").\n\n### 5eInitiativeYamlNoSource\n\nA minimal YAML snippet containing monster attributes required by the\nInitiative Tracker plugin. Use this in frontmatter.\n\nThe source book will not be included in the monster name.\n\n### 5eStatblockYaml\n\nComplete monster attributes in the format required by the Fantasy statblock plugin.\nUses double-quoted syntax to deal with a variety of characters occuring in\ntrait descriptions. Usable in frontmatter or Fantasy Statblock code blocks.\n\nThe source book will be included in the name if it isn't the default monster source (\"MM\").\n\n### 5eStatblockYamlNoSource\n\nComplete monster attributes in the format required by the Fantasy statblock plugin.\nUses double-quoted syntax to deal with a variety of characters occuring in\ntrait descriptions. Usable in frontmatter or Fantasy Statblock code blocks.\n\nThe source book will not be included in the monster name.\n\n### ac\n\nSee [AcHp#ac](../AcHp.md#ac)\n\n### acHp\n\nCreature AC and HP as [AcHp](../AcHp.md)\n\n### acText\n\nSee [AcHp#acText](../AcHp.md#actext)\n\n### action\n\nCreature actions as a list of [NamedText](../../NamedText.md)\n\n### alignment\n\nCreature alignment\n\n### allTraits\n\nCreature traits as [Traits](Traits.md)\n\n### bonusAction\n\nCreature bonus actions as a list of [NamedText](../../NamedText.md)\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### conditionImmune\n\nSee [ImmuneResist#conditionImmune](../ImmuneResist.md#conditionimmune)\n\n### cr\n\nChallenge rating\n\n### description\n\nFormatted text containing the creature description. Same as `{resource.text}`\n\n### environment\n\nFormatted text describing the creature's environment. Usually a single word.\n\n### fluffImages\n\nList of images as [ImageRef](../../ImageRef.md) (optional)\n\n### fullType\n\nCreature type (lowercase) and subtype if present: `{resource.type} ({resource.subtype})`\n\n### gear\n\nCreature gear as list of item links\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasImages\n\nReturn true if any images are present\n\n### hasMoreImages\n\nReturn true if more than one image is present\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### hitDice\n\nSee [AcHp#hitDice](../AcHp.md#hitdice)\n\n### hp\n\nSee [AcHp#hp](../AcHp.md#hp)\n\n### hpText\n\nSee [AcHp#hpText](../AcHp.md#hptext)\n\n### immune\n\nSee [ImmuneResist#immune](../ImmuneResist.md#immune)\n\n### immuneResist\n\nCreature immunities and resistances as [ImmuneResist](../ImmuneResist.md)\n\n### initiative\n\nInitiative bonus as [Initiative](Initiative.md)\n\n### isNpc\n\nTrue if this is an NPC\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### languages\n\nComma-separated string of languages the creature understands.\n\n### legendary\n\nCreature legendary traits as a list of [NamedText](../../NamedText.md)\n\n### legendaryGroup\n\nMap of grouped legendary traits (Lair Actions, Regional Effects, etc.).\nThe key the group name, and the value is a list of [NamedText](../../NamedText.md).\n\n### legendaryGroupLink\n\nMarkdown link to legendary group (can be embedded).\n\n### name\n\nNote name\n\n### passive\n\nPassive perception as a numerical value\n\n### pb\n\nProficiency bonus (modifier)\n\n### rawSpellcasting\n\nCreature spellcasting abilities as a list of [Spellcasting](Spellcasting.md)\nattributes\n\n### reaction\n\nCreature reactions as a list of [NamedText](../../NamedText.md)\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../../Reprinted.md))\n\n### resist\n\nSee [ImmuneResist#resist](../ImmuneResist.md#resist)\n\n### savesSkills\n\nCreature saving throws and skill modifiers as [SavesAndSkills](SavesAndSkills.md)\n\n### savingThrows\n\nString representation of saving throws.\nEquivalent to `{resource.savesSkills.saves}`\n\n### scores\n\nCreature ability scores as [AbilityScores](../AbilityScores/README.md)\n\n### senses\n\nComma-separated string of creature senses (if present).\n\n### showAllImages\n\nReturn embedded wikilinks for all images\nIf there is more than one, they will be displayed in a gallery.\n\n### showMoreImages\n\nReturn embedded wikilinks for all but the first image\nIf there is more than one, they will be displayed in a gallery.\n\n### showPortraitImage\n\nReturn an embedded wikilink to the first image\nWill have the \"right\" anchor tag.\n\n### size\n\nCreature size (capitalized)\n\n### skills\n\nString representation of saving throws.\nEquivalent to `{resource.savesSkills.skills}`\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### speed\n\nCreature speed as a comma-separated list\n\n### spellcasting\n\nAlways returns null/empty to suppress previous default behavior that\nrendered spellcasting as part of traits.\n\n2024 rules interleave spellcasting with traits, actions, bonus actions, etc.\n\n### subtype\n\nCreature subtype (lowercase)\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### token\n\nToken image as [ImageRef](../../ImageRef.md)\n\n### trait\n\nCreature traits as a list of [NamedText](../../NamedText.md)\n\n### type\n\nCreature type (lowercase)\n\n### vaultPath\n\nPath to this note in the vault\n\n### vulnerable\n\nSee [ImmuneResist#vulnerable](../ImmuneResist.md#vulnerable)\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteMonster/SavesAndSkills.md",
    "content": "# SavesAndSkills\n\n5eTools creature saving throws and skill attributes.\n\n## Attributes\n\n[saveOrDefault](#saveordefault), [saveValues](#savevalues), [saves](#saves), [skillChoices](#skillchoices), [skillValues](#skillvalues), [skills](#skills)\n\n### saveOrDefault\n\nSaving throws as a list of maps (for YAML Statblock)\n\n### saveValues\n\nSaving throws as a list of maps (for YAML Statblock)\n\n### saves\n\nCreature saving throws as a list: Constitution +6, Intelligence +8\n\n### skillChoices\n\nSometimes creatures have choices (one of the following...)\nThis is a list of lists of [SkillModifier](SkillModifier.md),\nwhere each sublist is a a group to choose from.\n\n### skillValues\n\nSkill modifiers as a list of maps (for YAML Statblock)\n\n### skills\n\nCreature skills as a list (with links)\n\n- `[History](..) +12, [Perception](...) +12`\n- `[History](..) +12; [Perception](...) +12; _One of_ [Athletics](...) +12 or [Acrobatics](...) +12`\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteMonster/SavingThrow.md",
    "content": "# SavingThrow\n\nSaving throw modifier.\n\nUsually an integer, but may be a \"special\" value (string, homebrew).\n\n## Attributes\n\n[ability](#ability), [modifier](#modifier), [special](#special)\n\n### ability\n\nAbility name, will be null if \"special\"\n\n### modifier\n\nModifier value. Will be 0 if unset or \"special\"\n\n### special\n\nEither the \"special\" value or a non-numeric modifier value\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteMonster/SkillModifier.md",
    "content": "# SkillModifier\n\nSkill modifier.\n\nUsually an integer, but may be a \"special\" value (string, homebrew).\n\n## Attributes\n\n[modifier](#modifier), [skill](#skill), [skillLink](#skilllink), [special](#special)\n\n### modifier\n\nModifier value. Will be 0 if unset or \"special\"\n\n### skill\n\nSkill name, will be null if \"special\"\n\n### skillLink\n\nSkill name as a link, will be null if \"special\"\n\n### special\n\nEither the \"special\" value or a non-numeric modifier value\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteMonster/Spellcasting.md",
    "content": "# Spellcasting\n\n5eTools creature spellcasting attributes.\n\nThis data object provides a default mechanism for creating\na marked up string based on the attributes that are present.\n\nTo use it, reference it directly:\n\n```md\n{#for spellcasting in resource.spellcasting}\n{spellcasting}\n{/for}\n```\n\nor, using `{#each}` instead:\n\n```md\n{#each resource.spellcasting}\n{it}\n{/each}\n```\n\n## Attributes\n\n[ability](#ability), [desc](#desc), [displayAs](#displayas), [fixed](#fixed), [footerEntries](#footerentries), [headerEntries](#headerentries), [hidden](#hidden), [name](#name), [spells](#spells), [variable](#variable)\n\n### ability\n\n\n### desc\n\nFormatted description: renders all attributes (except name) unless the trait is hidden\n\n### displayAs\n\nAttribute should be displayed as specified trait type. Values: `trait` (default), `action`, `bonus`, `reaction`,\n`legendary`\n\n### fixed\n\nSpells (links) that can be cast a fixed number of times (constant), at will (will), or as a ritual\n\n### footerEntries\n\nFormatted text that should be printed after the list of spells\n\n### headerEntries\n\nFormatted text that should be printed before the list of spells\n\n### hidden\n\nGroups that should be hidden. Values: `constant`, `will`, `rest`, `restLong`, `daily`, `weekly`, `monthly`, `yearly`,\n`ritual`, `spells`, `charges`, `recharge`, `legendary`\n\n### name\n\nName: \"Spellcasting\" or \"Innate Spellcasting\"\n\n### spells\n\nMap: key = spell level, value: spell level information as\n[Spells](Spells.md)\n\n### variable\n\nMap of frequency to spells (links).\n\nFrequencies (key)\n- charges\n- daily\n- legendary\n- monthly\n- recharge\n- rest\n- restLong\n- weekly\n- yearly\n\nValue is another map containing additional key/value pairs, where the key is a number,\nand the value is a list of spells (links).\n\nIf the key ends with `e` (like `1e` or `2e`), each will be appended, e.g. \"1/day each\"\nto specify that each spell can be cast once per day.\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteMonster/Spells.md",
    "content": "# Spells\n\n5eTools creature spell attributes (associated with a spell level)\n\n## Attributes\n\n[lowerBound](#lowerbound), [slots](#slots), [spells](#spells)\n\n### lowerBound\n\nSet if this represents a spell range (the associated key is the upper bound)\n\n### slots\n\nAvailable spell slots\n\n### spells\n\nList of spells (links)\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteMonster/TraitDescription.md",
    "content": "# TraitDescription\n\n5eTools creature trait description.\n\n## Attributes\n\n[description](#description), [present](#present), [title](#title), [traits](#traits)\n\n### description\n\nFormatted text describing the collection of traits\n\n### present\n\n\n### title\n\nTitle of the trait description\n\n### traits\n\nTraits as a list of [NamedText](../../NamedText.md)\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteMonster/Traits.md",
    "content": "# Traits\n\n5eTools creature traits.\n\n## Attributes\n\n[actions](#actions), [bonusActions](#bonusactions), [lairActions](#lairactions), [legendaryActions](#legendaryactions), [legendaryGroupLink](#legendarygrouplink), [mythicActions](#mythicactions), [reactions](#reactions), [regionalEffects](#regionaleffects), [traits](#traits)\n\n### actions\n\nCreature actions as a list of [NamedText](../../NamedText.md)\n\n### bonusActions\n\nCreature bonus actions as a list of [NamedText](../../NamedText.md)\n\n### lairActions\n\nCreature lair actions as a list of [NamedText](../../NamedText.md)\n\n### legendaryActions\n\nCreature legendary traits as a list of [NamedText](../../NamedText.md)\n\n### legendaryGroupLink\n\nLink to the legendary group, if present\n\n### mythicActions\n\nCreature mythic traits as a list of [NamedText](../../NamedText.md)\n\n### reactions\n\nCreature reactions as a list of [NamedText](../../NamedText.md)\n\n### regionalEffects\n\nCreature regional effects as a list of [NamedText](../../NamedText.md)\n\n### traits\n\nCreature traits as a list of [NamedText](../../NamedText.md)\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteObject.md",
    "content": "# QuteObject\n\n5eTools object attributes (`object2md.txt`)\n\nExtension of [Tools5eQuteBase](Tools5eQuteBase.md).\n\n## Attributes\n\n[5eInitiativeYaml](#5einitiativeyaml), [5eStatblockYaml](#5estatblockyaml), [ac](#ac), [acHp](#achp), [acText](#actext), [action](#action), [books](#books), [conditionImmune](#conditionimmune), [creatureType](#creaturetype), [fluffImages](#fluffimages), [getAliases](#getaliases), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [hitDice](#hitdice), [hp](#hp), [hpText](#hptext), [immune](#immune), [immuneResist](#immuneresist), [isNpc](#isnpc), [labeledSource](#labeledsource), [name](#name), [objectType](#objecttype), [reprintOf](#reprintof), [resist](#resist), [scores](#scores), [senses](#senses), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [size](#size), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [speed](#speed), [tags](#tags), [text](#text), [token](#token), [vaultPath](#vaultpath), [vulnerable](#vulnerable)\n\n### 5eInitiativeYaml\n\nA minimal YAML snippet containing object attributes required by the\nInitiative Tracker plugin. Use this in frontmatter.\n\n### 5eStatblockYaml\n\nComplete object attributes in the format required by the Fantasy statblock plugin.\nUses double-quoted syntax to deal with a variety of characters occuring in\ntrait descriptions. Usable in frontmatter or Fantasy Statblock code blocks.\n\n### ac\n\nSee [AcHp#ac](AcHp.md#ac)\n\n### acHp\n\nObject AC and HP as [AcHp](AcHp.md)\n\n### acText\n\nSee [AcHp#acText](AcHp.md#actext)\n\n### action\n\nObject actions as a list of [NamedText](../NamedText.md)\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### conditionImmune\n\nSee [ImmuneResist#conditionImmune](ImmuneResist.md#conditionimmune)\n\n### creatureType\n\nCreature type (lowercase); optional\n\n### fluffImages\n\nList of images as [ImageRef](../ImageRef.md) (optional)\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasImages\n\nReturn true if any images are present\n\n### hasMoreImages\n\nReturn true if more than one image is present\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### hitDice\n\nSee [AcHp#hitDice](AcHp.md#hitdice)\n\n### hp\n\nSee [AcHp#hp](AcHp.md#hp)\n\n### hpText\n\nSee [AcHp#hpText](AcHp.md#hptext)\n\n### immune\n\nSee [ImmuneResist#immune](ImmuneResist.md#immune)\n\n### immuneResist\n\nObject immunities and resistances as [ImmuneResist](ImmuneResist.md)\n\n### isNpc\n\nTrue if this is an NPC\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### name\n\nNote name\n\n### objectType\n\nObject type\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../Reprinted.md))\n\n### resist\n\nSee [ImmuneResist#resist](ImmuneResist.md#resist)\n\n### scores\n\nObject ability scores as [AbilityScores](AbilityScores/README.md))\n\n### senses\n\nComma-separated string of object senses (if present).\n\n### showAllImages\n\nReturn embedded wikilinks for all images\nIf there is more than one, they will be displayed in a gallery.\n\n### showMoreImages\n\nReturn embedded wikilinks for all but the first image\nIf there is more than one, they will be displayed in a gallery.\n\n### showPortraitImage\n\nReturn an embedded wikilink to the first image\nWill have the \"right\" anchor tag.\n\n### size\n\nObject size (capitalized)\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### speed\n\nObject speed as a comma-separated list\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### token\n\nToken image as [ImageRef](../ImageRef.md)\n\n### vaultPath\n\nPath to this note in the vault\n\n### vulnerable\n\nSee [ImmuneResist#vulnerable](ImmuneResist.md#vulnerable)\n"
  },
  {
    "path": "docs/templates/dnd5e/QutePsionic.md",
    "content": "# QutePsionic\n\n5eTools psionic talent attributes (`psionic2md.txt`)\n\nExtension of [Tools5eQuteBase](Tools5eQuteBase.md).\n\n## Attributes\n\n[books](#books), [fluffImages](#fluffimages), [focus](#focus), [getAliases](#getaliases), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [modes](#modes), [name](#name), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [typeOrder](#typeorder), [vaultPath](#vaultpath)\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### fluffImages\n\nList of images as [ImageRef](../ImageRef.md) (optional)\n\n### focus\n\nPsionic focus (string)\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasImages\n\nReturn true if any images are present\n\n### hasMoreImages\n\nReturn true if more than one image is present\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### modes\n\nPsionic mode as list of [NamedText](../NamedText.md)\n\n### name\n\nNote name\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../Reprinted.md))\n\n### showAllImages\n\nReturn embedded wikilinks for all images\nIf there is more than one, they will be displayed in a gallery.\n\n### showMoreImages\n\nReturn embedded wikilinks for all but the first image\nIf there is more than one, they will be displayed in a gallery.\n\n### showPortraitImage\n\nReturn an embedded wikilink to the first image\nWill have the \"right\" anchor tag.\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### typeOrder\n\nPsionic type and order (string)\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteRace.md",
    "content": "# QuteRace\n\n5eTools race attributes (`race2md.txt`)\n\nExtension of [Tools5eQuteBase](Tools5eQuteBase.md).\n\n## Attributes\n\n[ability](#ability), [books](#books), [cssClass](#cssclass), [description](#description), [fluffImages](#fluffimages), [getAliases](#getaliases), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [size](#size), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [speed](#speed), [spellcasting](#spellcasting), [tags](#tags), [text](#text), [traits](#traits), [type](#type), [vaultPath](#vaultpath)\n\n### ability\n\nAbility scores associated with this race (comma-separated list of scores or choices)\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### cssClass\n\nCSS class for this resource: `json5e-race` or `json5e-species`\n\n### description\n\nFormatted text describing the race. Optional. Same as {resource.text}\n\n### fluffImages\n\nList of images as [ImageRef](../ImageRef.md) (optional)\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasImages\n\nReturn true if any images are present\n\n### hasMoreImages\n\nReturn true if more than one image is present\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### name\n\nNote name\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../Reprinted.md))\n\n### showAllImages\n\nReturn embedded wikilinks for all images\nIf there is more than one, they will be displayed in a gallery.\n\n### showMoreImages\n\nReturn embedded wikilinks for all but the first image\nIf there is more than one, they will be displayed in a gallery.\n\n### showPortraitImage\n\nReturn an embedded wikilink to the first image\nWill have the \"right\" anchor tag.\n\n### size\n\nSize: Small or Medium\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### speed\n\nSpeed: 30 ft. May include additional values, like flight or swim speed.\n\n### spellcasting\n\nSpellcasting ability score\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### traits\n\nFormatted text with subsections describing racial traits\n\n### type\n\ntype of race or subrace (humanoid, ooze, undead, etc.)\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteReward.md",
    "content": "# QuteReward\n\n5eTools reward attributes (`reward2md.txt`)\n\nExtension of [Tools5eQuteBase](Tools5eQuteBase.md).\n\n## Attributes\n\n[ability](#ability), [books](#books), [detail](#detail), [fluffImages](#fluffimages), [getAliases](#getaliases), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [signatureSpells](#signaturespells), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath)\n\n### ability\n\nDescription of special ability granted by this reward, if defined separately. This is usually included in reward text.\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### detail\n\nReward detail string (similar to item detail). May include the reward type and rarity if either are defined.\n\n### fluffImages\n\nList of images as [ImageRef](../ImageRef.md) (optional)\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasImages\n\nReturn true if any images are present\n\n### hasMoreImages\n\nReturn true if more than one image is present\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### name\n\nNote name\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../Reprinted.md))\n\n### showAllImages\n\nReturn embedded wikilinks for all images\nIf there is more than one, they will be displayed in a gallery.\n\n### showMoreImages\n\nReturn embedded wikilinks for all but the first image\nIf there is more than one, they will be displayed in a gallery.\n\n### showPortraitImage\n\nReturn an embedded wikilink to the first image\nWill have the \"right\" anchor tag.\n\n### signatureSpells\n\nFormatted text describing sigature spells. Not commonly used.\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteSpell.md",
    "content": "# QuteSpell\n\n5eTools spell attributes (`spell2md.txt`)\n\nExtension of [Tools5eQuteBase](Tools5eQuteBase.md).\n\n## Attributes\n\n[abilityChecks](#abilitychecks), [affectsCreatureTypes](#affectscreaturetypes), [areaTags](#areatags), [backgrounds](#backgrounds), [books](#books), [classList](#classlist), [classes](#classes), [components](#components), [conditionImmune](#conditionimmune), [conditionInflict](#conditioninflict), [damageImmune](#damageimmune), [damageInflict](#damageinflict), [damageResist](#damageresist), [damageVulnerable](#damagevulnerable), [duration](#duration), [feats](#feats), [fluffImages](#fluffimages), [getAliases](#getaliases), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [higherLevels](#higherlevels), [labeledSource](#labeledsource), [level](#level), [miscTags](#misctags), [name](#name), [optionalfeatures](#optionalfeatures), [races](#races), [range](#range), [references](#references), [reprintOf](#reprintof), [ritual](#ritual), [savingThrows](#savingthrows), [scalingLevelDice](#scalingleveldice), [school](#school), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [spellAttacks](#spellattacks), [tags](#tags), [text](#text), [time](#time), [vaultPath](#vaultpath)\n\n### abilityChecks\n\nFormatted: Ability checks\n\n### affectsCreatureTypes\n\nFormatted: Creature types\n\n### areaTags\n\nFormatted/mapped: Areas\n\n### backgrounds\n\nString: rendered list of links to classes that grant access to this spell. May be incomplete or empty.\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### classList\n\nList of class names (not links) that can use this spell.\n\n### classes\n\nString: rendered list of links to classes that can use this spell. May be incomplete or empty.\n\n### components\n\nFormatted: spell components\n\n### conditionImmune\n\nFormatted: Condition immunities\n\n### conditionInflict\n\nFormatted: Conditions\n\n### damageImmune\n\nFormatted: Damage immunities\n\n### damageInflict\n\nFormatted: Damage types\n\n### damageResist\n\nFormatted: Damage resistances\n\n### damageVulnerable\n\nFormatted: Damage vulnerabilities\n\n### duration\n\nFormatted: spell range\n\n### feats\n\nString: rendered list of links to feats that grant acccess to this spell. May be incomplete or empty.\n\n### fluffImages\n\nList of images as [ImageRef](../ImageRef.md) (optional)\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasImages\n\nReturn true if any images are present\n\n### hasMoreImages\n\nReturn true if more than one image is present\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### higherLevels\n\nAt higher levels text\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### level\n\nSpell level\n\n### miscTags\n\nFormatted/mapped: Misc tags\n\n### name\n\nNote name\n\n### optionalfeatures\n\nString: rendered list of links to optional features that grant access to this spell. May be incomplete or empty.\n\n### races\n\nString: rendered list of links to races that can use this spell. May be incomplete or empty.\n\n### range\n\nFormatted: spell range\n\n### references\n\nList of links to resources (classes, subclasses, feats, etc.) that have access to this spell\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../Reprinted.md))\n\n### ritual\n\ntrue for ritual spells\n\n### savingThrows\n\nFormatted: Saving throws\n\n### scalingLevelDice\n\nFormatted: Scaling damage dice entries\n\n### school\n\nSpell school\n\n### showAllImages\n\nReturn embedded wikilinks for all images\nIf there is more than one, they will be displayed in a gallery.\n\n### showMoreImages\n\nReturn embedded wikilinks for all but the first image\nIf there is more than one, they will be displayed in a gallery.\n\n### showPortraitImage\n\nReturn an embedded wikilink to the first image\nWill have the \"right\" anchor tag.\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### spellAttacks\n\nFormatted: Spell attack forms\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### time\n\nFormatted: casting time\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteSubclass.md",
    "content": "# QuteSubclass\n\n5eTools subclass attributes (`subclass2md.txt`)\n\nExtension of [Tools5eQuteBase](Tools5eQuteBase.md).\n\n## Attributes\n\n[books](#books), [classProgression](#classprogression), [fluffImages](#fluffimages), [getAliases](#getaliases), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [parentClass](#parentclass), [parentClassLink](#parentclasslink), [parentClassSource](#parentclasssource), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [subclassTitle](#subclasstitle), [tags](#tags), [text](#text), [vaultPath](#vaultpath)\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### classProgression\n\nA pre-foramatted markdown callout describing subclass spell or feature progression\n\n### fluffImages\n\nList of images as [ImageRef](../ImageRef.md) (optional)\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasImages\n\nReturn true if any images are present\n\n### hasMoreImages\n\nReturn true if more than one image is present\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### name\n\nNote name\n\n### parentClass\n\nName of the parent class\n\n### parentClassLink\n\nMarkdown link to the parent class\n\n### parentClassSource\n\nSource of the parent class (abbreviation)\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../Reprinted.md))\n\n### showAllImages\n\nReturn embedded wikilinks for all images\nIf there is more than one, they will be displayed in a gallery.\n\n### showMoreImages\n\nReturn embedded wikilinks for all but the first image\nIf there is more than one, they will be displayed in a gallery.\n\n### showPortraitImage\n\nReturn an embedded wikilink to the first image\nWill have the \"right\" anchor tag.\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### subclassTitle\n\nTitle of subclass: \"Bard College\", or \"Primal Path\"\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteVehicle/README.md",
    "content": "# QuteVehicle\n\n5eTools vehicle attributes (`vehicle2md.txt`)\n\nSeveral different types of vehicle use this template, including:\nShip, spelljammer, infernal war manchie, objects and creatures.\nThey can have very different properties. Treat most as optional.\n\nExtension of [Tools5eQuteBase](../Tools5eQuteBase.md).\n\n## Attributes\n\n[action](#action), [books](#books), [fluffImages](#fluffimages), [getAliases](#getaliases), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [immuneResist](#immuneresist), [isCreature](#iscreature), [isObject](#isobject), [isShip](#isship), [isSpelljammer](#isspelljammer), [isWarMachine](#iswarmachine), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [scores](#scores), [shipCrewCargoPace](#shipcrewcargopace), [shipSections](#shipsections), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [sizeDimension](#sizedimension), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [terrain](#terrain), [text](#text), [token](#token), [vaultPath](#vaultpath), [vehicleType](#vehicletype)\n\n### action\n\nList of vehicle actions as a collection of [NamedText](../../NamedText.md)\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### fluffImages\n\nList of images as [ImageRef](../../ImageRef.md) (optional)\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasImages\n\nReturn true if any images are present\n\n### hasMoreImages\n\nReturn true if more than one image is present\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### immuneResist\n\nVehicle immunities and resistances as [ImmuneResist](../ImmuneResist.md)\n\n### isCreature\n\nTrue if this vehicle is a Creature\n\n### isObject\n\nTrue if this vehicle is an Object\n\n### isShip\n\nTrue if this vehicle is a Ship\n\n### isSpelljammer\n\nTrue if this vehicle is a Spelljammer\n\n### isWarMachine\n\nTrue if this vehicle is an Infernal War Machine\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### name\n\nNote name\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../../Reprinted.md))\n\n### scores\n\nObject ability scores as [AbilityScores](../AbilityScores/README.md)\nUsed by Ship, Infernal War Machine, Creature, Object\n\n### shipCrewCargoPace\n\nShip capacity and pace attributes as [ShipCrewCargoPace](ShipCrewCargoPace.md).\n\n### shipSections\n\nShip sections and traits as [ShipAcHp](ShipAcHp.md) (hull, sails,\noars, .. )\n\n### showAllImages\n\nReturn embedded wikilinks for all images\nIf there is more than one, they will be displayed in a gallery.\n\n### showMoreImages\n\nReturn embedded wikilinks for all but the first image\nIf there is more than one, they will be displayed in a gallery.\n\n### showPortraitImage\n\nReturn an embedded wikilink to the first image\nWill have the \"right\" anchor tag.\n\n### sizeDimension\n\nShip size and dimensions. Used by Ship, Infernal War Machine\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### terrain\n\nVehicle terrain as a comma-separated list (all)\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### token\n\nToken image as [ImageRef](../../ImageRef.md)\n\n### vaultPath\n\nPath to this note in the vault\n\n### vehicleType\n\nVehicle type: Ship, Spelljammer, Infernal War Machine, Creature, Object\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteVehicle/ShipAcHp.md",
    "content": "# ShipAcHp\n\n5eTools vehicle armor class and hit points attributes\n\nThis data object provides a default mechanism for creating\na marked up string based on the attributes that are present.\nTo use it, reference it directly.\n\n## Attributes\n\n[ac](#ac), [acText](#actext), [cost](#cost), [dt](#dt), [hitDice](#hitdice), [hp](#hp), [hpDiceRoller](#hpdiceroller), [hpText](#hptext), [mt](#mt)\n\n### ac\n\nArmor class (number)\n\n### acText\n\nAdditional armor class text. May link to related items\n\n### cost\n\nCost (per unit); preformatted string\n\n### dt\n\nDamage threshold; number\n\n### hitDice\n\nHit dice formula string: 7d10 + 14 (for creatures)\n\n### hp\n\nHit points (number or —)\n\n### hpDiceRoller\n\nHit points as a dice roller formula:\n\\`dice: 1d20+7|text(37)\\` (\\`1d20+7\\`)\n\n### hpText\n\nAdditional hit point text.\nIn the case of summoned creatures, this will contain notes for how hit points\nshould be calculated relative to the player's modifiers.\n\n### mt\n\nInfernal War Machine mishap threshold; number\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteVehicle/ShipCrewCargoPace.md",
    "content": "# ShipCrewCargoPace\n\n5eTools Ship crew, cargo, and pace attributes\n\nThis data object provides a default mechanism for creating\na marked up string based on the attributes that are present.\n\nTo use it, reference it directly:\n\n```md\n{#if resource.shipCrewCargoPace}\n{resource.shipCrewCargoPace}\n{/if}\n```\n\n## Attributes\n\n[acHp](#achp), [cargo](#cargo), [crew](#crew), [crewText](#crewtext), [keelBeam](#keelbeam), [passenger](#passenger), [shipPace](#shippace), [speedPace](#speedpace)\n\n### acHp\n\nSpelljammer or Infernal War Machine HP/AC\n\n### cargo\n\nCargo capacity (string)\n\n### crew\n\nCrew capacity (number)\n\n### crewText\n\nAdditional crew notes\n\n### keelBeam\n\nSpelljammer Keel/Beam\n\n### passenger\n\nPassenger capacity (number)\n\n### shipPace\n\nShip pace (number, mph)\nShip speed is pace * 10 (*Special Travel Pace*, DMG p242).\n\n### speedPace\n\nSpelljammer speed and pace (preformatted string)\n"
  },
  {
    "path": "docs/templates/dnd5e/QuteVehicle/ShipSection.md",
    "content": "# ShipSection\n\n5eTools vehicle sections\n\nThis data object provides a default mechanism for creating\na marked up string based on the attributes that are present.\nTo use it, reference it directly.\n\n## Attributes\n\n[acHp](#achp), [actions](#actions), [desc](#desc), [name](#name), [speed](#speed)\n\n### acHp\n\nArmor class and hit points as [ShipAcHp](ShipAcHp.md)\n\n### actions\n\nPre-formatted actions\n\n### desc\n\nPre-formatted text description\n\n### name\n\nName\n\n### speed\n\nSpeed as a list of pre-formatted strings\n"
  },
  {
    "path": "docs/templates/dnd5e/README.md",
    "content": "# 5eTools templates\n\nQute templates for generating content from 5eTools data.\n\n## References\n\n- [AbilityScores](AbilityScores/README.md): 5eTools Ability Score attributes.\n- [AcHp](AcHp.md): 5eTools armor class and hit points attributes\n\n    This data object provides a default mechanism for creating\n    a marked up string based on the attributes that are present.\n\n- [ImmuneResist](ImmuneResist.md): 5eTools vulnerabilities, resistances, immunities, and condition immunities\n\n    This data object provides a default mechanism for creating\n    a marked up string based on the attributes that are present.\n\n- [QuteBackground](QuteBackground.md): 5eTools background attributes (`background2md.txt`).\n- [QuteBastion](QuteBastion/README.md): 5eTools background attributes (`bastion2md.txt`).\n- [QuteClass](QuteClass/README.md): 5eTools class attributes (`class2md.txt`)\n\n    Extension of [Tools5eQuteBase](Tools5eQuteBase.md).\n\n- [QuteDeck](QuteDeck/README.md): 5eTools deck attributes (`deck2md.txt`)\n\n    Extension of [Tools5eQuteBase](Tools5eQuteBase.md).\n\n- [QuteDeity](QuteDeity.md): 5eTools deity attributes (`deity2md.txt`)\n\n    Extension of [Tools5eQuteBase](Tools5eQuteBase.md).\n\n- [QuteFeat](QuteFeat.md): 5eTools feat and optional feat attributes (`feat2md.txt`)\n\n    Extension of [Tools5eQuteBase](Tools5eQuteBase.md).\n\n- [QuteHazard](QuteHazard.md): 5eTools hazard attributes (`hazard2md.txt`)\n\n    Extension of [Tools5eQuteBase](Tools5eQuteBase.md).\n\n- [QuteItem](QuteItem/README.md): 5eTools item attributes (`item2md.txt`)\n\n    Extension of [Tools5eQuteBase](Tools5eQuteBase.md).\n\n- [QuteMonster](QuteMonster/README.md): 5eTools creature attributes (`monster2md.txt`)\n\n    Extension of [Tools5eQuteBase](Tools5eQuteBase.md).\n\n- [QuteObject](QuteObject.md): 5eTools object attributes (`object2md.txt`)\n\n    Extension of [Tools5eQuteBase](Tools5eQuteBase.md).\n\n- [QutePsionic](QutePsionic.md): 5eTools psionic talent attributes (`psionic2md.txt`)\n\n    Extension of [Tools5eQuteBase](Tools5eQuteBase.md).\n\n- [QuteRace](QuteRace.md): 5eTools race attributes (`race2md.txt`)\n\n    Extension of [Tools5eQuteBase](Tools5eQuteBase.md).\n\n- [QuteReward](QuteReward.md): 5eTools reward attributes (`reward2md.txt`)\n\n    Extension of [Tools5eQuteBase](Tools5eQuteBase.md).\n\n- [QuteSpell](QuteSpell.md): 5eTools spell attributes (`spell2md.txt`)\n\n    Extension of [Tools5eQuteBase](Tools5eQuteBase.md).\n\n- [QuteSubclass](QuteSubclass.md): 5eTools subclass attributes (`subclass2md.txt`)\n\n    Extension of [Tools5eQuteBase](Tools5eQuteBase.md).\n\n- [QuteVehicle](QuteVehicle/README.md): 5eTools vehicle attributes (`vehicle2md.txt`)\n\n    Several different types of vehicle use this template, including:\n    Ship, spelljammer, infernal war manchie, objects and creatures.\n\n- [Tools5eQuteBase](Tools5eQuteBase.md): Attributes for notes that are generated from the 5eTools data.\n- [Tools5eQuteNote](Tools5eQuteNote.md): Attributes for notes that are generated from the 5eTools data.\n"
  },
  {
    "path": "docs/templates/dnd5e/Tools5eQuteBase.md",
    "content": "# Tools5eQuteBase\n\nAttributes for notes that are generated from the 5eTools data.\nThis is a trivial extension of [QuteBase](../QuteBase.md).\n\nNotes created from `Tools5eQuteBase` will use a specific template\nfor the type. For example, `QuteBackground` will use `background2md.txt`.\n\n## Attributes\n\n[books](#books), [fluffImages](#fluffimages), [getAliases](#getaliases), [hasImages](#hasimages), [hasMoreImages](#hasmoreimages), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [showAllImages](#showallimages), [showMoreImages](#showmoreimages), [showPortraitImage](#showportraitimage), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath)\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### fluffImages\n\nList of images as [ImageRef](../ImageRef.md) (optional)\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasImages\n\nReturn true if any images are present\n\n### hasMoreImages\n\nReturn true if more than one image is present\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### name\n\nNote name\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../Reprinted.md))\n\n### showAllImages\n\nReturn embedded wikilinks for all images\nIf there is more than one, they will be displayed in a gallery.\n\n### showMoreImages\n\nReturn embedded wikilinks for all but the first image\nIf there is more than one, they will be displayed in a gallery.\n\n### showPortraitImage\n\nReturn an embedded wikilink to the first image\nWill have the \"right\" anchor tag.\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/dnd5e/Tools5eQuteNote.md",
    "content": "# Tools5eQuteNote\n\nAttributes for notes that are generated from the 5eTools data.\nThis is a trivial extension of [QuteNote](../QuteNote.md).\n\nNotes created from `Tools5eQuteNote` will use the `note2md.txt` template.\n\n## Attributes\n\n[books](#books), [getAliases](#getaliases), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath)\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### name\n\nNote name\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../Reprinted.md))\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/pf2e/Pf2eQuteBase.md",
    "content": "# Pf2eQuteBase\n\nAttributes for notes that are generated from the Pf2eTools data.\nThis is a trivial extension of [QuteBase](../QuteBase.md).\n\nNotes created from `Pf2eQuteBase` will use a specific template\nfor the type. For example, `QuteBackground` will use `background2md.txt`.\n\n## Attributes\n\n[books](#books), [getAliases](#getaliases), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath)\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### name\n\nNote name\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../Reprinted.md))\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/pf2e/Pf2eQuteNote.md",
    "content": "# Pf2eQuteNote\n\nAttributes for notes that are generated from the Pf2eTools data.\nThis is a trivial extension of [QuteNote](../QuteNote.md).\n\nNotes created from `Pf2eQuteNote` will use the `note2md.txt` template\nunless otherwise noted. Folder index notes use `index2md.txt`.\n\n## Attributes\n\n[books](#books), [getAliases](#getaliases), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath)\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### name\n\nNote name\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../Reprinted.md))\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/pf2e/QuteAbility.md",
    "content": "# QuteAbility\n\nPf2eTools Ability attributes (`ability2md.txt` or `inline-ability2md.txt`).\n\nAbilities are rendered both standalone and inline (as an admonition block).\nThe default template can render both. It contains some special syntax to handle\nthe inline case.\n\nUse `%%--` to mark the end of the preamble (frontmatter and\nother leading content only appropriate to the standalone case).\n\nExtension of [Pf2eQuteNote](Pf2eQuteNote.md)\n\n## Attributes\n\n[activity](#activity), [bareTraitList](#baretraitlist), [books](#books), [components](#components), [cost](#cost), [embedded](#embedded), [frequency](#frequency), [getAliases](#getaliases), [hasActivity](#hasactivity), [hasAttributes](#hasattributes), [hasDetails](#hasdetails), [hasEffect](#haseffect), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [note](#note), [prerequisites](#prerequisites), [range](#range), [reference](#reference), [reprintOf](#reprintof), [requirements](#requirements), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [special](#special), [tags](#tags), [text](#text), [traits](#traits), [trigger](#trigger), [vaultPath](#vaultpath)\n\n### activity\n\nAbility ([activity/activation details](QuteDataActivity.md))\n\n### bareTraitList\n\nReturn a comma-separated list of de-styled trait links (no title attributes)\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### components\n\nList of formatted strings. Activation components for this ability, e.g. command, envision\n\n### cost\n\nThe cost of using this ability\n\n### embedded\n\nTrue if this ability is embedded in another note (admonition block).\nWhen this is true, the `inline-ability` template is used.\n\n### frequency\n\n[QuteDataFrequency](QuteDataFrequency.md).\nHow often this ability can be used/activated. Use directly to get a formatted string.\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasActivity\n\nTrue if an activity (with text), components, or traits are present.\n\n### hasAttributes\n\nTrue if hasActivity is true, hasEffect is true or cost is present.\nIn other words, this is true if a list of attributes could have been rendered.\n\nUse this to test for the end of those attributes (add whitespace or a special\ncharacter ahead of ability text)\n\n### hasDetails\n\nTrue if the ability is a short, one-line name and description.\nUse this to test to choose between a detailed or simple rendering.\n\n### hasEffect\n\nTrue if frequency, trigger, and requirements are present. In other words, this is true if the ability has an effect.\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### name\n\nNote name\n\n### note\n\nAny additional notes related to this ability that aren't included in the other fields.\n\n### prerequisites\n\nFormatted string. Prerequisites before this ability can be activated or taken.\n\n### range\n\n[QuteDataRange](QuteDataRange/README.md). The targeting range for this ability.\n\n### reference\n\nA formatted string which is a link to the base ability that this ability references. Embedded only.\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../Reprinted.md))\n\n### requirements\n\nFormatted string. Requirements for activating this ability\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### special\n\nSpecial notes for this ability - usually requirements or caveats relating to its use.\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### traits\n\nCollection of trait links. Use `{#for}` or `{#each}` to iterate over the collection.\nSee [traitList](#traitlist) or [bareTraitList](#baretraitlist).\n\n### trigger\n\nFormatted string. Trigger to activate this ability\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/pf2e/QuteAbilityOrAffliction.md",
    "content": "# QuteAbilityOrAffliction\n\nA union type which is either a [QuteAbility](QuteAbility.md)\nor a [QuteAffliction](QuteAffliction/README.md).\n\nUse [isAbility](#ability)\nand [isAffliction](#affliction)\nto tell whether it's an ability or an affliction.\n\n## Attributes\n\n[ability](#ability), [affliction](#affliction)\n\n### ability\n\nReturns true if this object is a [QuteAbility](QuteAbility.md)\n\n### affliction\n\nReturns true if this object is a [QuteAffliction](QuteAffliction/README.md)\n"
  },
  {
    "path": "docs/templates/pf2e/QuteAction/ActionType.md",
    "content": "# ActionType\n\nPf2eTools Action type attributes.\n\nThis data object provides a default mechanism for creating\na marked up string based on the attributes that are present.\nTo use it, reference this attribute directly: `{resource.actionType}`.\n\n## Attributes\n\n[ancestry](#ancestry), [archetype](#archetype), [basic](#basic), [classType](#classtype), [heritage](#heritage), [item](#item), [skills](#skills), [subclass](#subclass), [variantrule](#variantrule), [versatileHeritage](#versatileheritage)\n\n### ancestry\n\nList of ancestries associated with this action\n\n### archetype\n\nList of archetypes associated with this action\n\n### basic\n\nTrue if this is a basic action\n\n### classType\n\nList of classes associated with this action\n\n### heritage\n\nList of heritages associated with this action\n\n### item\n\nTrue if this an item action\n\n### skills\n\nSkills used or required by this action\n\n### subclass\n\nList of subclasses associated with this action\n\n### variantrule\n\nList of variant rules associated with this action\n\n### versatileHeritage\n\nList of versatile heritages associated with this action\n"
  },
  {
    "path": "docs/templates/pf2e/QuteAction/README.md",
    "content": "# QuteAction\n\nPf2eTools Action attributes (`action2md.txt`)\n\nExtension of [Pf2eQuteBase](../Pf2eQuteBase.md)\n\n## Attributes\n\n[actionType](#actiontype), [activity](#activity), [altNames](#altnames), [basic](#basic), [books](#books), [cost](#cost), [frequency](#frequency), [getAliases](#getaliases), [hasSections](#hassections), [item](#item), [labeledSource](#labeledsource), [name](#name), [prerequisites](#prerequisites), [reprintOf](#reprintof), [requirements](#requirements), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [traits](#traits), [trigger](#trigger), [vaultPath](#vaultpath)\n\n### actionType\n\nType of action (as [ActionType](ActionType.md))\n\n### activity\n\nActivity/Activation cost (as [QuteDataActivity](../QuteDataActivity.md))\n\n### altNames\n\n\n### basic\n\nTrue if this is a basic action. Same as `{resource.actionType.basic}`.\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### cost\n\nThe cost of using this action\n\n### frequency\n\n[QuteDataFrequency](../QuteDataFrequency.md).\nHow often this action can be used/activated. Use directly to get a formatted string.\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### item\n\nTrue if this action is an item action. Same as `{resource.actionType.item}`.\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### name\n\nNote name\n\n### prerequisites\n\nPrerequisite trait or characteristic for performing this action\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../../Reprinted.md))\n\n### requirements\n\nSituational requirements for performing this action\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### traits\n\nCollection of traits (decorated links)\n\n### trigger\n\nTrigger for this action\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/pf2e/QuteAffliction/QuteAfflictionSave.md",
    "content": "# QuteAfflictionSave\n\nAffliction saving throw\n\n## Attributes\n\n[notes](#notes), [save](#save), [value](#value)\n\n### notes\n\nAny notes relating to the saving throw\n\n### save\n\nThe type of save associated with the throw e.g. Fortitude\n\n### value\n\nThe DC of the saving throw\n"
  },
  {
    "path": "docs/templates/pf2e/QuteAffliction/QuteAfflictionStage.md",
    "content": "# QuteAfflictionStage\n\nPf2eTools affliction stage attributes.\n\n## Attributes\n\n[duration](#duration), [text](#text)\n\n### duration\n\nFormatted text. Affliction duration\n\n### text\n\nFormatted text. Affliction stage\n"
  },
  {
    "path": "docs/templates/pf2e/QuteAffliction/README.md",
    "content": "# QuteAffliction\n\nPf2eTools Affliction attributes (inline/embedded, `inline-affliction2md.txt`)\n\nExtension of [Pf2eQuteNote](../Pf2eQuteNote.md)\n\n## Attributes\n\n[altNames](#altnames), [books](#books), [category](#category), [effect](#effect), [getAliases](#getaliases), [hasSections](#hassections), [isEmbedded](#isembedded), [labeledSource](#labeledsource), [level](#level), [maxDuration](#maxduration), [name](#name), [notes](#notes), [onset](#onset), [reprintOf](#reprintof), [savingThrow](#savingthrow), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [stages](#stages), [tags](#tags), [temptedCurse](#temptedcurse), [text](#text), [traits](#traits), [vaultPath](#vaultpath)\n\n### altNames\n\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### category\n\nCategory of affliction (Curse or Disease). Usually shown alongside the level.\n\n### effect\n\nFormatted text. Affliction effect, may be multiple lines.\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### isEmbedded\n\nIf true, then this affliction is embedded into a larger note.\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### level\n\nInteger from 1 to 10. Level of the affliction.\n\n### maxDuration\n\nFormatted text. Maximum duration of the infliction.\n\n### name\n\nNote name\n\n### notes\n\nAny additional notes associated with the affliction.\n\n### onset\n\nFormatted text. Maximum duration of the infliction.\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../../Reprinted.md))\n\n### savingThrow\n\nThe saving throw required to not contract or advance the affliction as\n[QuteAfflictionSave](QuteAfflictionSave.md)\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### stages\n\nAffliction stages: map of name to stage data as\n[QuteAfflictionStage](QuteAfflictionStage.md)\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### temptedCurse\n\nA description of the tempted version of the curse\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### traits\n\nCollection of traits (decorated links)\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/pf2e/QuteArchetype.md",
    "content": "# QuteArchetype\n\nPf2eTools Archetype attributes (`archetype2md.txt`)\n\nExtension of [Pf2eQuteBase](Pf2eQuteBase.md)\n\n## Attributes\n\n[benefits](#benefits), [books](#books), [dedicationLevel](#dedicationlevel), [feats](#feats), [getAliases](#getaliases), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [traits](#traits), [vaultPath](#vaultpath)\n\n### benefits\n\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### dedicationLevel\n\n\n### feats\n\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### name\n\nNote name\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../Reprinted.md))\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### traits\n\nCollection of traits (decorated links)\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/pf2e/QuteBackground.md",
    "content": "# QuteBackground\n\nPf2eTools Background attributes (`background2md.txt`)\n\nExtension of [Pf2eQuteBase](Pf2eQuteBase.md)\n\n## Attributes\n\n[books](#books), [getAliases](#getaliases), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath)\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### name\n\nNote name\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../Reprinted.md))\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/pf2e/QuteBook/BookInfo.md",
    "content": "# BookInfo\n\nPf2eTools book information\n\nThis data object provides a default mechanism for creating\na marked up string based on the attributes that are present.\nTo use it, reference it directly: `{resource.actionType}`.\n\n## Attributes\n\n[author](#author), [cover](#cover), [group](#group), [id](#id), [name](#name), [published](#published)\n\n### author\n\nAuthor\n\n### cover\n\nCover image as `dev.ebullient.convert.qute.ImageRef`\n\n### group\n\nGroup this book belongs to (core, lost-omens, supplement, etc.)\n\n### id\n\nBook id\n\n### name\n\nName of the book\n\n### published\n\nDate published\n"
  },
  {
    "path": "docs/templates/pf2e/QuteBook/README.md",
    "content": "# QuteBook\n\nPf2eTools Book attributes (`book2md.txt`)\n\nExtension of [Pf2eQuteNote](../Pf2eQuteNote.md)\n\n## Attributes\n\n[altNames](#altnames), [bookInfo](#bookinfo), [books](#books), [getAliases](#getaliases), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath)\n\n### altNames\n\n\n### bookInfo\n\nInformation about the book as `dev.ebullient.convert.tools.pf2e.qute.QuteBook.BookInfo`\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### name\n\nNote name\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../../Reprinted.md))\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/pf2e/QuteCreature/CreatureAbilities.md",
    "content": "# CreatureAbilities\n\nA creature's abilities, split into the section of the statblock where they should be displayed. Each section is\na list of [QuteAbilityOrAffliction](../QuteAbilityOrAffliction.md). Using an entry in one of these lists directly\nwill give you a pre-formatted ability according to the embedded template defined for [QuteAbility](../QuteAbility.md) or\n[QuteAffliction](../QuteAffliction/README.md) as appropriate.\n\n## Attributes\n\n[bottom](#bottom), [middle](#middle), [top](#top)\n\n### bottom\n\nAbilities which should be displayed in the bottom section of the statblock\n\n### middle\n\nAbilities which should be displayed in the middle section of the statblock\n\n### top\n\nAbilities which should be displayed in the top section of the statblock\n"
  },
  {
    "path": "docs/templates/pf2e/QuteCreature/CreatureLanguages.md",
    "content": "# CreatureLanguages\n\nThe languages and language features known by a creature. Example default output:\n`Common, Sylvan; telepathy 100ft; knows any language the summoner does`\n\n## Attributes\n\n[abilities](#abilities), [languages](#languages), [notes](#notes)\n\n### abilities\n\nLanguage-related abilities (optional)\n\n### languages\n\nLanguages known (optional)\n\n### notes\n\nLanguage-related notes (optional)\n"
  },
  {
    "path": "docs/templates/pf2e/QuteCreature/CreatureRitualCasting.md",
    "content": "# CreatureRitualCasting\n\nInformation about a type of ritual casting available to this creature.\n\n## Attributes\n\n[dc](#dc), [ranks](#ranks), [tradition](#tradition)\n\n### dc\n\nThe spell save DC for these rituals\n\n### ranks\n\nThe ritual ranks, as a list of [CreatureSpells](CreatureSpells.md)\n\n### tradition\n\nThe tradition for these rituals\n"
  },
  {
    "path": "docs/templates/pf2e/QuteCreature/CreatureSense.md",
    "content": "# CreatureSense\n\nA creature's senses. Example default output: `tremorsense (imprecise) 20ft`\n\n## Attributes\n\n[name](#name), [range](#range), [type](#type)\n\n### name\n\nThe name of the sense (required, string)\n\n### range\n\nThe range of the sense (optional, integer)\n\n### type\n\nThe type of the sense - e.g. precise, imprecise (optional, string)\n"
  },
  {
    "path": "docs/templates/pf2e/QuteCreature/CreatureSkills.md",
    "content": "# CreatureSkills\n\nA creature's skill information. Example default output:\n\n```md\nAthletics +10, Cult Lore +10 (lore on their cult), Stealth +10 (+12 in forests); Some skill note\n```\n\n## Attributes\n\n[notes](#notes), [skills](#skills)\n\n### notes\n\nNotes for the creature's skills (list of strings, optional)\n\n### skills\n\nSkill bonuses for the creature, as a list of\n[QuteDataNamedBonus](../QuteDataGenericStat/QuteDataNamedBonus.md)\n"
  },
  {
    "path": "docs/templates/pf2e/QuteCreature/CreatureSpellReference.md",
    "content": "# CreatureSpellReference\n\nA spell known by the creature.\n\n```md\n[shadow siphon](#) (acid only) (×2)\n```\n\n## Attributes\n\n[amount](#amount), [link](#link), [name](#name), [notes](#notes)\n\n### amount\n\nThe number of casts available for this spell. A value of 0 represents an at will spell. Use\n[CreatureSpellReference#formattedAmount](#formattedamount) to get this as a formatted string.\n\n### link\n\nA formatted link to the spell's note, or just the spell's name if we couldn't get a link.\n\n### name\n\nThe name of the spell\n\n### notes\n\nAny notes associated with this spell, e.g. \"at will only\"\n"
  },
  {
    "path": "docs/templates/pf2e/QuteCreature/CreatureSpellcasting.md",
    "content": "# CreatureSpellcasting\n\nInformation about a type of spellcasting available to this creature.\n\n## Attributes\n\n[attackBonus](#attackbonus), [constantRanks](#constantranks), [customName](#customname), [dc](#dc), [focusPoints](#focuspoints), [formattedStats](#formattedstats), [name](#name), [notes](#notes), [preparation](#preparation), [ranks](#ranks), [tradition](#tradition)\n\n### attackBonus\n\nThe spell attack bonus for these spells (integer)\n\n### constantRanks\n\nThe constant spells for each rank, as a list of [CreatureSpells](CreatureSpells.md)\n\n### customName\n\nA custom name for this set of spells, e.g. \"Champion Devotion Spells\". Use\n[CreatureSpellcasting#name](#name) to get a name which takes this into account\nif it exists.\n\n### dc\n\nThe spell save DC for these spells (integer)\n\n### focusPoints\n\nThe number of focus points available to this creature for these spells. Present only if these\nare focus spells.\n\n### formattedStats\n\nStats for this kind of spellcasting, including the DC, attack bonus, and any focus points.\n\n```md\nDC 20, attack +25, 2 Focus Points\n```\n\n### name\n\nThe name for this set of spells. This is either the custom name, or derived from the tradition and\npreparation - e.g. \"Occult Prepared Spells\", or \"Divine Innate Spells\".\n\n### notes\n\nAny notes associated with these spells\n\n### preparation\n\nThe type of preparation for these spells, as a [SpellcastingPreparation](SpellcastingPreparation.md)\n\n### ranks\n\nThe spells for each rank, as a list of [CreatureSpells](CreatureSpells.md).\n\n### tradition\n\nThe tradition for these spells, as a [SpellcastingTradition](SpellcastingTradition.md)\n"
  },
  {
    "path": "docs/templates/pf2e/QuteCreature/CreatureSpells.md",
    "content": "# CreatureSpells\n\nA collection of spells with some additional information.\n\n```md\n**Cantrips (9th)** [daze](#), [shadow siphon](#) (acid only) (×2)\n```\n\n```md\n**4th** [confusion](#), [phantasmal killer](#) (2 slots)\n```\n\n## Attributes\n\n[cantripRank](#cantriprank), [cantrips](#cantrips), [knownRank](#knownrank), [slots](#slots), [spells](#spells)\n\n### cantripRank\n\nThe rank that these spells are auto-heightened to. Present only for cantrips.\n\n### cantrips\n\nTrue if these are cantrip spells\n\n### knownRank\n\nThe rank that these spells are known at (0 for cantrips). May be absent for rituals.\n\n### slots\n\nThe number of slots available for these spells. Not present for constant spells or rituals.\n\n### spells\n\nA list of spells, as a list of [CreatureSpellReference](CreatureSpellReference.md)\n"
  },
  {
    "path": "docs/templates/pf2e/QuteCreature/README.md",
    "content": "# QuteCreature\n\nPf2eTools Creature attributes (`creature2md.txt`)\n\nExtension of [Pf2eQuteBase](../Pf2eQuteBase.md)\n\n## Attributes\n\n[abilities](#abilities), [abilityMods](#abilitymods), [altNames](#altnames), [attacks](#attacks), [books](#books), [defenses](#defenses), [description](#description), [getAliases](#getaliases), [hasSections](#hassections), [items](#items), [labeledSource](#labeledsource), [languages](#languages), [level](#level), [name](#name), [perception](#perception), [reprintOf](#reprintof), [ritualCasting](#ritualcasting), [senses](#senses), [skills](#skills), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [speed](#speed), [spellcasting](#spellcasting), [tags](#tags), [text](#text), [traits](#traits), [vaultPath](#vaultpath)\n\n### abilities\n\nThe creature's abilities, as a\n[CreatureAbilities](CreatureAbilities.md).\n\n### abilityMods\n\nAbility modifiers as a map of (name, modifier)\n\n### altNames\n\n\n### attacks\n\nThe creature's attacks, as a list of [QuteInlineAttack](../QuteInlineAttack/README.md)\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### defenses\n\nDefenses (AC, saves, etc) as [QuteDataDefenses](../QuteDataDefenses/README.md)\n\n### description\n\nShort creature description (optional)\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### items\n\nItems held by the creature as a list of strings\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### languages\n\nLanguages as [CreatureLanguages](CreatureLanguages.md)\n\n### level\n\nCreature level (number, optional)\n\n### name\n\nNote name\n\n### perception\n\nCreature perception (number, optional)\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../../Reprinted.md))\n\n### ritualCasting\n\nThe creature's ritual casting capabilities, as a list of [CreatureRitualCasting](CreatureRitualCasting.md)\n\n### senses\n\nSenses as a list of [CreatureSense](CreatureSense.md)\n\n### skills\n\nSkill bonuses as [CreatureSkills](CreatureSkills.md)\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### speed\n\nThe creature's speed, as an [QuteDataSpeed](../QuteDataSpeed.md)\n\n### spellcasting\n\nThe creature's spellcasting capabilities, as a list of [CreatureSpellcasting](CreatureSpellcasting.md)\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### traits\n\nCollection of traits (decorated links, optional)\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/pf2e/QuteCreature/SpellcastingPreparation.md",
    "content": "# SpellcastingPreparation\n\n\n## Attributes\n"
  },
  {
    "path": "docs/templates/pf2e/QuteCreature/SpellcastingTradition.md",
    "content": "# SpellcastingTradition\n\n\n## Attributes\n"
  },
  {
    "path": "docs/templates/pf2e/QuteDataActivity.md",
    "content": "# QuteDataActivity\n\nPf2eTools activity attributes. This attribute will render itself as a formatted link:\n\n<pre>\n[textGlyph](rulesPath \"glyph.title\")<optional text>\n</pre>\n\n## Attributes\n\n[glyph](#glyph), [rulesPath](#rulespath), [text](#text), [textGlyph](#textglyph)\n\n### glyph\n\nicon/image representing this activity as a [ImageRef](../ImageRef.md)\n\n### rulesPath\n\nThe path which leads to an explanation of this particular activity\n\n### text\n\nThe text associated with the action - may be null.\n\n### textGlyph\n\nA textual representation of the glyph, used as the link text\n"
  },
  {
    "path": "docs/templates/pf2e/QuteDataArmorClass.md",
    "content": "# QuteDataArmorClass\n\nPf2eTools armor class attributes.\n\nDefault representation example:\n\n```md\n**AC** 15 (10 with mage armor) note ability\n```\n\n## Attributes\n\n[abilities](#abilities), [alternateValues](#alternatevalues), [notes](#notes), [value](#value)\n\n### abilities\n\nAny AC related abilities\n\n### alternateValues\n\nAlternate AC values as a map of (condition, AC value)\n\n### notes\n\nAny notes associated with the AC e.g. \"with mage armor\"\n\n### value\n\nThe AC value\n"
  },
  {
    "path": "docs/templates/pf2e/QuteDataDefenses/QuteSavingThrows.md",
    "content": "# QuteSavingThrows\n\nPathfinder 2e saving throws. Example default rendering:\n\n```md\n**Fort** +10 (+12 vs. poison), **Ref** +5 (+7 vs. traps), **Will** +4 (+6 vs. mental); +1 status to\nall saves vs. magic\n```\n\n## Attributes\n\n[abilities](#abilities), [fort](#fort), [ref](#ref), [will](#will)\n\n### abilities\n\nAny saving throw related abilities\n\n### fort\n\nFortitude saving throw bonus, as a [QuteDataNamedBonus](../QuteDataGenericStat/QuteDataNamedBonus.md)\n\n### ref\n\nReflex saving throw bonus, as a [QuteDataNamedBonus](../QuteDataGenericStat/QuteDataNamedBonus.md)\n\n### will\n\nWill saving throw bonus, as a [QuteDataNamedBonus](../QuteDataGenericStat/QuteDataNamedBonus.md)\n"
  },
  {
    "path": "docs/templates/pf2e/QuteDataDefenses/README.md",
    "content": "# QuteDataDefenses\n\nPf2eTools Armor class, Saving Throws, and other attributes describing defenses of a creature or hazard.\n\nExample:\n\n```md\n**AC** 23 (33 with mage armor); **Fort** +15, **Ref** +12, **Will** +10\n```\n\n```md\n**Floor Hardness** 18, **Floor HP** 72 (BT 36);\n**Channel Hardness** 12, **Channel HP** 48 (BT24 ) to destroy a channel gate;\n**Immunities** critical hits;\n**Resistances** precision damage;\n**Weaknesses** bludgeoning damage\n```\n\n## Attributes\n\n[ac](#ac), [additionalHpHardnessBt](#additionalhphardnessbt), [hpHardnessBt](#hphardnessbt), [immunities](#immunities), [resistances](#resistances), [savingThrows](#savingthrows), [weaknesses](#weaknesses)\n\n### ac\n\nThe armor class as a [QuteDataArmorClass](../QuteDataArmorClass.md)\n\n### additionalHpHardnessBt\n\nAdditional HP, hardness, or broken thresholds for other HP components as a map of\nnames to [QuteDataHpHardnessBt](../QuteDataHpHardnessBt/README.md)\n\n### hpHardnessBt\n\nHP, hardness, and broken threshold stored in a [QuteDataHpHardnessBt](../QuteDataHpHardnessBt/README.md)\n\n### immunities\n\nList of strings, optional\n\n### resistances\n\nMap of (name, [QuteDataGenericStat](../QuteDataGenericStat/README.md))\n\n### savingThrows\n\nThe saving throws, as [QuteSavingThrows](QuteSavingThrows.md)\n\n### weaknesses\n\nMap of (name, [QuteDataGenericStat](../QuteDataGenericStat/README.md))\n"
  },
  {
    "path": "docs/templates/pf2e/QuteDataDuration.md",
    "content": "# QuteDataDuration\n\nA duration of time. This may be either a [QuteDataTimedDuration](QuteDataTimedDuration/README.md), which represents a period of time longer\nthan an activity, or a [QuteDataActivity](QuteDataActivity.md). Use [QuteDataDuration#Activity](#activity) to check whether this\nduration is an activity.\n\nUsing this directly will give the default representation for either object.\n\n## Attributes\n\n[activity](#activity)\n\n### activity\n\nReturns true if this duration is a [QuteDataActivity](QuteDataActivity.md).\n"
  },
  {
    "path": "docs/templates/pf2e/QuteDataFrequency.md",
    "content": "# QuteDataFrequency\n\nA description of a frequency e.g. \"once\", which may include an interval that this is repeated for.\n\nExamples:\n\n- once per day\n- once per hour\n- 3 times per day\n- `recurs=true`: once every day\n- `overcharge=true`: once per day, plus overcharge\n- `interval=2`: once per 2 days\n\n## Attributes\n\n[interval](#interval), [notes](#notes), [overcharge](#overcharge), [recurs](#recurs), [unit](#unit), [value](#value)\n\n### interval\n\nThe interval that the frequency is repeated for\n\n### notes\n\nAny notes associated with the frequency. May include a custom string, for frequencies which cannot be\nrepresented using the normal parts. If this is present, then the other parameters will be null.\n\n### overcharge\n\nWhether there's an overcharge involved. Used for wands mostly. In the default representation, this\nadds \", plus overcharge\".\n\n### recurs\n\nWhether the unit recurs. In the default representation, this makes it render \"every\" instead of \"per\"\n\n### unit\n\nThe unit the frequency is in, string. Required.\n\n### value\n\nThe number represented by the frequency, integer\n"
  },
  {
    "path": "docs/templates/pf2e/QuteDataGenericStat/QuteDataNamedBonus.md",
    "content": "# QuteDataNamedBonus\n\nA Pathfinder 2e named bonus, potentially with other conditional bonuses.\n\nExample default representation:\n`Stealth +36 (+42 in forests) (ignores tremorsense)`\n\n## Attributes\n\n[name](#name), [notes](#notes), [otherBonuses](#otherbonuses), [value](#value)\n\n### name\n\nThe name of the skill\n\n### notes\n\nAny notes associated with this skill bonus\n\n### otherBonuses\n\nAny additional bonuses, as a map of descriptions to bonuses. Iterate over all map entries to\ndisplay the values, e.g.: `{#each resource.skills.otherBonuses}{it.key}: {it.value}{/each}`\n\n### value\n\nThe standard bonus associated with this skill\n"
  },
  {
    "path": "docs/templates/pf2e/QuteDataGenericStat/README.md",
    "content": "# QuteDataGenericStat\n\nA generic container for a PF2e stat value which may have an attached note.\n\n## Attributes\n"
  },
  {
    "path": "docs/templates/pf2e/QuteDataGenericStat/SimpleStat.md",
    "content": "# SimpleStat\n\nA basic [QuteDataGenericStat](README.md) which provides\nonly a value and possibly a note.\n\nDefault representation: `10 (some note) (some other note)`\n\n## Attributes\n"
  },
  {
    "path": "docs/templates/pf2e/QuteDataHpHardnessBt/HpStat.md",
    "content": "# HpStat\n\nHP value and associated notes. Referencing this directly provides a default representation, e.g.\n`15 to destroy a head (head regrowth)`\n\n## Attributes\n\n[abilities](#abilities), [notes](#notes), [value](#value)\n\n### abilities\n\nAny abilities associated with the HP\n\n### notes\n\nAny notes associated with the HP.\n\n### value\n\nThe HP value itself\n"
  },
  {
    "path": "docs/templates/pf2e/QuteDataHpHardnessBt/README.md",
    "content": "# QuteDataHpHardnessBt\n\nHit Points, Hardness, and a broken threshold for hazards and shields. Used for creatures, hazards, and shields.\n\nHazard example with a broken threshold and note:\n\n```md\n**Hardness** 10, **HP (BT)** 30 (15) to destroy a channel gate\n```\n\nHazard example with a name, broken threshold, and note:\n\n```md\n**Floor Hardness** 10, **Floor HP** 30 (BT 15) to destroy a channel gate\n```\n\nCreature example with a name and ability:\n\n```md\n**Head Hardness** 10, **Head HP** 30 (hydra regeneration)\n```\n\n## Attributes\n\n[brokenThreshold](#brokenthreshold), [hardness](#hardness), [hp](#hp)\n\n### brokenThreshold\n\nBroken threshold as an integer (optional, not populated for creatures)\n\n### hardness\n\nHardness as a [SimpleStat](../QuteDataGenericStat/SimpleStat.md)\n(optional)\n\n### hp\n\nThe HP as a [HpStat](HpStat.md) (optional)\n"
  },
  {
    "path": "docs/templates/pf2e/QuteDataRange/README.md",
    "content": "# QuteDataRange\n\nA range with a given value and unit of measurement for that value.\n\n## Attributes\n\n[notes](#notes), [unit](#unit), [value](#value)\n\n### notes\n\nAny associated notes, or an alternate rendering when the range can't be represented using just\na unit and value.\n\n### unit\n\nWhat unit of measurement the `value` is given in, as a [RangeUnit](RangeUnit.md)\n\n### value\n\nAn integer value for the range\n"
  },
  {
    "path": "docs/templates/pf2e/QuteDataRange/RangeUnit.md",
    "content": "# RangeUnit\n\n\n## Attributes\n"
  },
  {
    "path": "docs/templates/pf2e/QuteDataSpeed.md",
    "content": "# QuteDataSpeed\n\nExamples:\n\n- `10 feet, swim 20 feet (some note); some ability`\n- `10 feet, swim 20 feet, some ability`\n\n## Attributes\n\n[abilities](#abilities), [notes](#notes), [otherSpeeds](#otherspeeds), [value](#value)\n\n### abilities\n\nAny speed-related abilities\n\n### notes\n\nAny speed-related notes\n\n### otherSpeeds\n\nOther speeds, as a map of (name, speed in feet)\n\n### value\n\nThe land speed in feet\n"
  },
  {
    "path": "docs/templates/pf2e/QuteDataTimedDuration/DurationUnit.md",
    "content": "# DurationUnit\n\nRepresents different units that a duration might be in.\n\n## Attributes\n"
  },
  {
    "path": "docs/templates/pf2e/QuteDataTimedDuration/README.md",
    "content": "# QuteDataTimedDuration\n\nA duration of time, represented by a numerical value and a unit. Sometimes this includes a custom display string,\nfor durations which cannot be represented using the normal structure.\n\nExamples:\n\n- A duration of 3 minutes: `3 minutes`\n- A duration of 1 turn: `until the end of your next turn`\n- An unlimited duration: `unlimited`\n\n## Attributes\n\n[customDisplay](#customdisplay), [formattedNotes](#formattednotes), [notes](#notes), [unit](#unit), [value](#value)\n\n### customDisplay\n\nThe custom display used for this duration.\n\n### formattedNotes\n\nReturns a comma delimited string containing all notes.\n\n### notes\n\n\n### unit\n\nThe unit that the quantity is measured in, as a [DurationUnit](DurationUnit.md)\n\n### value\n\nThe quantity of time\n"
  },
  {
    "path": "docs/templates/pf2e/QuteDeity/QuteDeityCleric.md",
    "content": "# QuteDeityCleric\n\nPf2eTools cleric divine attributes\n\nThis data object provides a default mechanism for creating\na marked up string based on the attributes that are present.\nTo use it, reference it directly: `{resource.actionType}`.\n\n## Attributes\n\n[alternateDomains](#alternatedomains), [divineAbility](#divineability), [divineFont](#divinefont), [divineSkill](#divineskill), [domains](#domains), [favoredWeapon](#favoredweapon), [spells](#spells)\n\n### alternateDomains\n\n\n### divineAbility\n\n\n### divineFont\n\n\n### divineSkill\n\n\n### domains\n\n\n### favoredWeapon\n\n\n### spells\n"
  },
  {
    "path": "docs/templates/pf2e/QuteDeity/QuteDivineAvatar.md",
    "content": "# QuteDivineAvatar\n\nPf2eTools avatar attributes\n\nThis data object provides a default mechanism for creating\na marked up string based on the attributes that are present.\nTo use it, reference it directly: `{resource.actionType}`.\n\n## Attributes\n\n[abilities](#abilities), [ability](#ability), [attacks](#attacks), [name](#name), [preface](#preface), [shield](#shield), [speed](#speed)\n\n### abilities\n\n\n### ability\n\n\n### attacks\n\n\n### name\n\n\n### preface\n\n\n### shield\n\n\n### speed\n\nThe avatar's speed, as a [QuteDataSpeed](../QuteDataSpeed.md)\n"
  },
  {
    "path": "docs/templates/pf2e/QuteDeity/QuteDivineIntercession.md",
    "content": "# QuteDivineIntercession\n\nPf2eTools divine intercession attributes.\n\nThis data object provides a default mechanism for creating\na marked up string based on the attributes that are present.\nTo use it, reference it directly: `{resource.actionType}`.\n\n## Attributes\n\n[flavor](#flavor), [majorBoon](#majorboon), [majorCurse](#majorcurse), [minorBoon](#minorboon), [minorCurse](#minorcurse), [moderateBoon](#moderateboon), [moderateCurse](#moderatecurse), [source](#source)\n\n### flavor\n\n\n### majorBoon\n\n\n### majorCurse\n\n\n### minorBoon\n\n\n### minorCurse\n\n\n### moderateBoon\n\n\n### moderateCurse\n\n\n### source\n"
  },
  {
    "path": "docs/templates/pf2e/QuteDeity/README.md",
    "content": "# QuteDeity\n\nPf2eTools Deity attributes (`deity2md.txt`)\n\nDeities are rendered both standalone and inline (as an admonition block).\nThe default template can render both.\nIt uses special syntax to handle the inline case.\n\nUse `%%--` to mark the end of the preamble (frontmatter and\nother leading content only appropriate to the standalone case).\n\nExtension of [Pf2eQuteBase](../Pf2eQuteBase.md)\n\n## Attributes\n\n[alignment](#alignment), [altNames](#altnames), [anathema](#anathema), [areasOfConcern](#areasofconcern), [avatar](#avatar), [books](#books), [category](#category), [cleric](#cleric), [edicts](#edicts), [followerAlignment](#followeralignment), [getAliases](#getaliases), [hasSections](#hassections), [intercession](#intercession), [labeledSource](#labeledsource), [name](#name), [pantheon](#pantheon), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath)\n\n### alignment\n\n\n### altNames\n\n\n### anathema\n\n\n### areasOfConcern\n\n\n### avatar\n\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### category\n\n\n### cleric\n\n\n### edicts\n\n\n### followerAlignment\n\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### intercession\n\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### name\n\nNote name\n\n### pantheon\n\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../../Reprinted.md))\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/pf2e/QuteFeat.md",
    "content": "# QuteFeat\n\nPf2eTools Feat attributes (`feat2md.txt`)\n\nFeats are rendered both standalone and inline (as an admonition block).\nThe default template can render both.\nIt uses special syntax to handle the inline case.\n\nUse `%%--` to mark the end of the preamble (frontmatter and\nother leading content only appropriate to the standalone case).\n\nExtension of [Pf2eQuteBase](Pf2eQuteBase.md)\n\n## Attributes\n\n[access](#access), [activity](#activity), [altNames](#altnames), [books](#books), [cost](#cost), [embedded](#embedded), [frequency](#frequency), [getAliases](#getaliases), [hasSections](#hassections), [labeledSource](#labeledsource), [leadsTo](#leadsto), [level](#level), [name](#name), [note](#note), [prerequisites](#prerequisites), [reprintOf](#reprintof), [requirements](#requirements), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [special](#special), [tags](#tags), [text](#text), [traits](#traits), [trigger](#trigger), [vaultPath](#vaultpath)\n\n### access\n\n\n### activity\n\nActivity/Activation cost (as [QuteDataActivity](QuteDataActivity.md))\n\n### altNames\n\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### cost\n\n\n### embedded\n\nTrue if this ability is embedded in another note (admonition block).\nThe default template uses this flag to include a `title:` prefix for the admonition block:  \n\n`{#if resource.embedded }title: {#else}# {/if}{resource.name}` *\n\n### frequency\n\n[QuteDataFrequency](QuteDataFrequency.md).\nHow often this feat can be used/activated. Use directly to get a formatted string.\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### leadsTo\n\n\n### level\n\n\n### name\n\nNote name\n\n### note\n\n\n### prerequisites\n\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../Reprinted.md))\n\n### requirements\n\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### special\n\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### traits\n\nCollection of traits (decorated links)\n\n### trigger\n\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/pf2e/QuteHazard/QuteHazardStealth.md",
    "content": "# QuteHazardStealth\n\nPf2eTools hazard attributes.\n\nThis data object provides a default mechanism for creating\na marked up string based on the attributes that are present.\n\n## Attributes\n\n[dc](#dc), [minProf](#minprof), [notes](#notes), [value](#value)\n\n### dc\n\nThe DC which must be passed to see the hazard\n\n### minProf\n\nThe minimum Perception proficiency required to be able to roll against the hazard's Stealth\n\n### notes\n\nAny notes associated with the hazard's Stealth. Sometimes this includes other stats which may\nbe rolled against the hazard's Stealth.\n\n### value\n\nThe hazard's Stealth bonus\n"
  },
  {
    "path": "docs/templates/pf2e/QuteHazard/README.md",
    "content": "# QuteHazard\n\nPf2eTools Hazard attributes (`hazard2md.txt`)\n\nHazards are rendered both standalone and inline (as an admonition block).\nThe default template can render both.\nIt uses special syntax to handle the inline case.\n\nUse `%%--` to mark the end of the preamble (frontmatter and\nother leading content only appropriate to the standalone case).\n\nExtension of [Pf2eQuteBase](../Pf2eQuteBase.md)\n\n## Attributes\n\n[abilities](#abilities), [actions](#actions), [attacks](#attacks), [books](#books), [complexity](#complexity), [defenses](#defenses), [disable](#disable), [getAliases](#getaliases), [hasSections](#hassections), [labeledSource](#labeledsource), [level](#level), [name](#name), [perception](#perception), [reprintOf](#reprintof), [reset](#reset), [routine](#routine), [routineAdmonition](#routineadmonition), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [stealth](#stealth), [tags](#tags), [text](#text), [traits](#traits), [vaultPath](#vaultpath)\n\n### abilities\n\nThe hazard's abilities, as a list of\n[QuteAbility](../QuteAbility.md)\n\n### actions\n\nThe hazard's actions, as a list of\n[QuteAbilityOrAffliction](../QuteAbilityOrAffliction.md).\n\nUsing the elements directly will give a default rendering, but if you want more\ncontrol you can use `isAffliction` and `isAbility` to check whether it's an affliction or an\nability. Example:\n\n```md\n{#each resource.actions}\n{#if it.isAffliction}\n\n**Affliction** {it}\n{#else if it.isAbility}\n\n**Ability** {it}\n{/if}\n{/each}\n```\n\n### attacks\n\nThe attacks available to the hazard, as a list of\n[QuteInlineAttack](../QuteInlineAttack/README.md)\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### complexity\n\n\n### defenses\n\n\n### disable\n\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### level\n\n\n### name\n\nNote name\n\n### perception\n\nThe hazard's perception, as a\n[QuteDataGenericStat](../QuteDataGenericStat/README.md)\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../../Reprinted.md))\n\n### reset\n\n\n### routine\n\n\n### routineAdmonition\n\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### stealth\n\nThe hazard's stealth, as a\n[QuteHazardAttributes](QuteHazardStealth.md)\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### traits\n\nCollection of traits (decorated links)\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/pf2e/QuteInlineAttack/AttackRangeType.md",
    "content": "# AttackRangeType\n\n\n## Attributes\n"
  },
  {
    "path": "docs/templates/pf2e/QuteInlineAttack/README.md",
    "content": "# QuteInlineAttack\n\nPf2eTools Attack attributes (inline/embedded, `inline-attack2md.txt`)\n\nWhen used directly, renders according to `inline-attack2md.txt`\n\n## Attributes\n\n[activity](#activity), [attackBonus](#attackbonus), [damage](#damage), [damageTypes](#damagetypes), [effects](#effects), [multilineEffect](#multilineeffect), [name](#name), [notes](#notes), [rangeType](#rangetype), [traits](#traits)\n\n### activity\n\nNumber/type of action cost ([QuteDataActivity](../QuteDataActivity.md))\n\n### attackBonus\n\nThe to-hit bonus for the attack (integer)\n\n### damage\n\nDamage if the attack hits (formatted string), e.g. \"1d8 bludgeoning plus grab\". This will include\ndamage types and non-multiline effects.\n\n### damageTypes\n\nThe damage types caused by the attack. Will be included in either\n[damage](#damage) or in\n[multilineEffect](#multilineeffect).\n\n### effects\n\nAny additional effects associated with the attack e.g. grab (list of strings). Effects listed here\nmay be repeated in [damage](#damage).\n\n### multilineEffect\n\nA multi-line effect. Formatted string, will be null if there is no multiline effect.\n\n### name\n\nThe name of the attack e.g. \"fist\" (string)\n\n### notes\n\nAny notes associated with the attack e.g. \"no multiple attack penalty\" (list of strings)\n\n### rangeType\n\nThe range of the attack ([AttackRangeType](AttackRangeType.md) enum)\n\n### traits\n\nAny traits associated with the attack (collection of decorated links)\n"
  },
  {
    "path": "docs/templates/pf2e/QuteItem/QuteItemActivate.md",
    "content": "# QuteItemActivate\n\nPf2eTools item activation attributes.\n\nThis data object provides a default mechanism for creating\na marked up string based on the attributes that are present.\nTo use it, reference it directly: `{resource.activate}`.\n\n## Attributes\n\n[activity](#activity), [components](#components), [frequency](#frequency), [requirements](#requirements), [trigger](#trigger)\n\n### activity\n\nItem [activity/activation details](../QuteDataActivity.md)\n\n### components\n\nFormatted string. Components required to activate this item\n\n### frequency\n\n[QuteDataFrequency](../QuteDataFrequency.md).\nHow often this item can be used/activated. Use directly to get a formatted string.\n\n### requirements\n\nFormatted string. Requirements for activating this item\n\n### trigger\n\nFormatted string. Trigger to activate this item\n"
  },
  {
    "path": "docs/templates/pf2e/QuteItem/QuteItemArmorData.md",
    "content": "# QuteItemArmorData\n\nPf2eTools item armor attributes\n\nThis data object provides a default mechanism for creating\na marked up string based on the attributes that are present.\nTo use it, reference it directly: `{resource.armor}`.\n\n## Attributes\n\n[ac](#ac), [checkPenalty](#checkpenalty), [dexCap](#dexcap), [speedPenalty](#speedpenalty), [strength](#strength)\n\n### ac\n\n[Item armor class details](../QuteDataArmorClass.md)\n\n### checkPenalty\n\nFormatted string. Check penalty\n\n### dexCap\n\nFormatted string. Dex cap\n\n### speedPenalty\n\nFormatted string. Speed penalty\n\n### strength\n\nFormatted string. Armor strength\n"
  },
  {
    "path": "docs/templates/pf2e/QuteItem/QuteItemShieldData.md",
    "content": "# QuteItemShieldData\n\nPf2eTools item shield attributes. When referenced directly, provides a default formatting, e.g.\n\n```md\n**AC Bonus** +2; **Speed Penalty** —; **Hardness** 3; **HP (BT)** 12 (6)\n```\n\n## Attributes\n\n[ac](#ac), [hpHardnessBt](#hphardnessbt), [speedPenalty](#speedpenalty)\n\n### ac\n\nAC bonus for the shield, as [QuteDataArmorClass](../QuteDataArmorClass.md)\n(required)\n\n### hpHardnessBt\n\nHP, hardness, and broken threshold of the shield, as\n[QuteDataHpHardnessBt](../QuteDataHpHardnessBt/README.md)\n(required)\n\n### speedPenalty\n\nSpeed penalty for the shield, as a formatted string (string, required)\n"
  },
  {
    "path": "docs/templates/pf2e/QuteItem/QuteItemVariant.md",
    "content": "# QuteItemVariant\n\nPf2eTools item variant attributes\n\nThis data object provides a default mechanism for creating\na marked up string based on the attributes that are present.\n\nTo use it, reference it directly:\n\n```md\n{#for variants in resource.variants}\n{variants}\n{/for}\n```\n\nor, using `{#each}` instead:\n\n```md\n{#each resource.variants}\n{it}\n{/each}\n```\n\n## Attributes\n\n[craftReq](#craftreq), [entries](#entries), [level](#level), [price](#price), [variantType](#varianttype)\n\n### craftReq\n\n\n### entries\n\n\n### level\n\n\n### price\n\n\n### variantType\n"
  },
  {
    "path": "docs/templates/pf2e/QuteItem/QuteItemWeaponData.md",
    "content": "# QuteItemWeaponData\n\nPf2eTools item weapon attributes\n\nThis data object provides a default mechanism for creating\na marked up string based on the attributes that are present.\n\nTo use it, reference it directly:\n\n```md\n{#for weapons in resource.weapons}\n{weapons}\n{/for}\n```\n\nor, using `{#each}` instead:\n\n```md\n{#each resource.weapons}\n{it}\n{/each}\n```\n\n## Attributes\n\n[damage](#damage), [group](#group), [ranged](#ranged), [traits](#traits), [type](#type)\n\n### damage\n\n\n### group\n\n\n### ranged\n\n\n### traits\n\nFormatted string. List of traits (links)\n\n### type\n\nFormatted string. Weapon type\n"
  },
  {
    "path": "docs/templates/pf2e/QuteItem/README.md",
    "content": "# QuteItem\n\nPf2eTools Item attributes\n\nExtension of [Pf2eQuteBase](../Pf2eQuteBase.md)\n\n## Attributes\n\n[access](#access), [activate](#activate), [altNames](#altnames), [ammunition](#ammunition), [armor](#armor), [books](#books), [category](#category), [contract](#contract), [craftReq](#craftreq), [duration](#duration), [getAliases](#getaliases), [group](#group), [hands](#hands), [hasSections](#hassections), [labeledSource](#labeledsource), [level](#level), [name](#name), [onset](#onset), [price](#price), [reprintOf](#reprintof), [shield](#shield), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [traits](#traits), [usage](#usage), [variants](#variants), [vaultPath](#vaultpath), [weapons](#weapons)\n\n### access\n\nFormatted string. Item access attributes\n\n### activate\n\nItem activation attributes as [QuteItemActivate](QuteItemActivate.md)\n\n### altNames\n\n\n### ammunition\n\nFormatted string. Ammunition required\n\n### armor\n\nItem armor attributes as [QuteItemArmorData](QuteItemArmorData.md)\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### category\n\nFormatted string. Item category\n\n### contract\n\nItem contract attributes as a list of [NamedText](../../NamedText.md)\n\n### craftReq\n\nFormatted string. Crafting requirements\n\n### duration\n\nFormatted string. How long will the item remain active\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### group\n\nFormatted string. Item group\n\n### hands\n\nFormatted string. How many hands does this item require to use\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### level\n\nFormatted string. Item power level\n\n### name\n\nNote name\n\n### onset\n\nFormatted string. Onset attributes\n\n### price\n\nFormatted string. Item price (pp, gp, sp, cp)\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../../Reprinted.md))\n\n### shield\n\nItem shield attributes as [QuteItemShieldData](QuteItemShieldData.md)\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### traits\n\nCollection of traits (decorated links)\n\n### usage\n\nItem use attributes as a list of [NamedText](../../NamedText.md)\n\n### variants\n\nItem variants as list of [QuteItemVariant](QuteItemVariant.md)\n\n### vaultPath\n\nPath to this note in the vault\n\n### weapons\n\nItem weapon attributes as list of [QuteItemWeaponData](QuteItemWeaponData.md)\n"
  },
  {
    "path": "docs/templates/pf2e/QuteRitual/QuteRitualCasting.md",
    "content": "# QuteRitualCasting\n\nPf2eTools ritual casting attributes\n\nThis data object provides a default mechanism for creating\na marked up string based on the attributes that are present.\nTo use it, reference it directly: `{resource.casting}`.\n\n## Attributes\n\n[cost](#cost), [duration](#duration), [secondaryCasters](#secondarycasters)\n\n### cost\n\nFormatted string. Material cost of the spell\n\n### duration\n\nDuration to cast, as a [QuteDataDuration](../QuteDataDuration.md) which is either a [QuteDataActivity](../QuteDataActivity.md), or a\n[QuteDataTimedDuration](../QuteDataTimedDuration/README.md).\n\n### secondaryCasters\n\nMinumum number of secondary casters required\n"
  },
  {
    "path": "docs/templates/pf2e/QuteRitual/QuteRitualChecks.md",
    "content": "# QuteRitualChecks\n\nPf2eTools ritual check attributes\n\nThis data object provides a default mechanism for creating\na marked up string based on the attributes that are present.\nTo use it, reference it directly: `{resource.checks}`.\n\n## Attributes\n\n[primaryChecks](#primarychecks), [secondaryChecks](#secondarychecks)\n\n### primaryChecks\n\nFormatted string. Links to skills for primary checks\n\n### secondaryChecks\n\nFormatted string. Links to skills for secondary checks\n"
  },
  {
    "path": "docs/templates/pf2e/QuteRitual/README.md",
    "content": "# QuteRitual\n\nPf2eTools Ritual attributes (`ritual2md.txt`)\n\nExtension of [Pf2eQuteBase](../Pf2eQuteBase.md)\n\n## Attributes\n\n[altNames](#altnames), [books](#books), [casting](#casting), [checks](#checks), [duration](#duration), [getAliases](#getaliases), [hasSections](#hassections), [heightened](#heightened), [labeledSource](#labeledsource), [level](#level), [name](#name), [reprintOf](#reprintof), [requirements](#requirements), [ritualType](#ritualtype), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [targeting](#targeting), [text](#text), [traits](#traits), [vaultPath](#vaultpath)\n\n### altNames\n\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### casting\n\nCasting attributes as [QuteRitualCasting](QuteRitualCasting.md)\n\n### checks\n\nCasting attributes as [QuteRitualChecks](QuteRitualChecks.md)\n\n### duration\n\nFormated text. Ritual duration\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### heightened\n\nHeightened spell effects as a list of [Traits](../../NamedText.md)\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### level\n\nA spell’s overall power, from 1 to 10.\n\n### name\n\nNote name\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../../Reprinted.md))\n\n### requirements\n\nFormatted text. Ritual requirements\n\n### ritualType\n\nType: Ritual (usually)\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### targeting\n\nCasting attributes as [QuteSpellTarget](../QuteSpell/QuteSpellTarget.md)\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### traits\n\nCollection of traits (decorated links)\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/pf2e/QuteSpell/QuteSpellAmp.md",
    "content": "# QuteSpellAmp\n\nPf2eTools spell Amp attributes\n\nThis attribute will render itself as labeled elements\nif you reference it directly: `{resource.amp}`.\n\n## Attributes\n\n[ampEffects](#ampeffects), [text](#text)\n\n### ampEffects\n\nHeightened amp effects as a list of [Traits](../../NamedText.md)\n\n### text\n\nFormatted text describing amp effects\n"
  },
  {
    "path": "docs/templates/pf2e/QuteSpell/QuteSpellDuration.md",
    "content": "# QuteSpellDuration\n\nDetails about the duration of the spell.\n\nExample default representations:\n\n- `1 minute`\n- `sustained up to 1 minute`\n\n## Attributes\n\n[dismissable](#dismissable), [duration](#duration), [sustained](#sustained)\n\n### dismissable\n\nWhether this spell can be dismissed, boolean. Not included in the default representation.\n\n### duration\n\nThe duration of this spell, as a [QuteDataTimedDuration](../QuteDataTimedDuration/README.md).\n\n### sustained\n\nWhether this is a sustained spell, boolean\n"
  },
  {
    "path": "docs/templates/pf2e/QuteSpell/QuteSpellSave.md",
    "content": "# QuteSpellSave\n\nDetails about the saving throw for a spell.\n\nExample default representations:\n\n- `basic Reflex or Fortitude`\n- `basic Reflex, Fortitude, or Willpower`\n\n## Attributes\n\n[basic](#basic), [hidden](#hidden), [saves](#saves)\n\n### basic\n\nTrue if this is a basic save (boolean)\n\n### hidden\n\nWhether this save should be hidden. This is sometimes true when it's a special save that is\ndescribed in the text of the spell.\n\n### saves\n\nThe saving throws that can be used for this spell (list of strings)\n"
  },
  {
    "path": "docs/templates/pf2e/QuteSpell/QuteSpellTarget.md",
    "content": "# QuteSpellTarget\n\nPf2eTools spell target attributes.\n\nThis attribute will render itself as labeled elements\nif you reference it directly: `{resource.targeting}`.\n\n## Attributes\n\n[area](#area), [range](#range), [targets](#targets)\n\n### area\n\nFormatted string describing the spell area of effect\n\n### range\n\nThe spell's range, as a [QuteDataRange](../QuteDataRange/README.md).\n\n### targets\n\nFormatted string describing the spell target(s)\n"
  },
  {
    "path": "docs/templates/pf2e/QuteSpell/README.md",
    "content": "# QuteSpell\n\nPf2eTools Spell attributes (`spell2md.txt`)\n\nExtension of [Pf2eQuteBase](../Pf2eQuteBase.md)\n\n## Attributes\n\n[altNames](#altnames), [amp](#amp), [books](#books), [castDuration](#castduration), [components](#components), [cost](#cost), [domains](#domains), [duration](#duration), [formattedComponents](#formattedcomponents), [getAliases](#getaliases), [hasSections](#hassections), [heightened](#heightened), [labeledSource](#labeledsource), [level](#level), [name](#name), [reprintOf](#reprintof), [requirements](#requirements), [save](#save), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [spellLists](#spelllists), [spellType](#spelltype), [subclass](#subclass), [tags](#tags), [targeting](#targeting), [text](#text), [traditions](#traditions), [traits](#traits), [trigger](#trigger), [vaultPath](#vaultpath)\n\n### altNames\n\n\n### amp\n\nPsi amp behavior as [QuteSpellAmp](QuteSpellAmp.md)\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### castDuration\n\nThe time it takes to cast the spell, as a [QuteDataDuration](../QuteDataDuration.md) which is either a [QuteDataActivity](../QuteDataActivity.md)\nor a [QuteDataTimedDuration](../QuteDataTimedDuration/README.md).\n\n### components\n\nThe required spell components as a list of formatted strings (maybe empty). Use\n[QuteSpell#formattedComponents](#formattedcomponents)\nto get a pre-formatted representation.\n\n### cost\n\nThe material cost of the spell as a formatted string (optional)\n\n### domains\n\nList of spell domains (links)\n\n### duration\n\nSpell duration, as [QuteDataTimedDuration](../QuteDataTimedDuration/README.md)\n\n### formattedComponents\n\nThe components required for the spell, as a formatted string. Example:\n\n```md\n[somatic](#), [verbal](#)\n```\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### heightened\n\nHeightened spell effects as a list of [NamedText](../../NamedText.md)\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### level\n\nA spell’s overall power, from 1 to 10.\n\n### name\n\nNote name\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../../Reprinted.md))\n\n### requirements\n\nThe requirements to cast the spell (optional)\n\n### save\n\nSpell save, as [QuteSpellSave](QuteSpellSave.md)\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### spellLists\n\nSpell lists containing this spell\n\n### spellType\n\nType: spell, cantrip, or focus\n\n### subclass\n\nList of category (Bloodline or Mystery) to Subclass (Sorcerer or Oracle). Link to class (if present)\nas a list of [NamedText](../../NamedText.md).\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### targeting\n\nSpell target attributes as [QuteSpellTarget](QuteSpellTarget.md)\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### traditions\n\nList of spell traditions (trait links)\n\n### traits\n\nCollection of traits (decorated links)\n\n### trigger\n\nThe activation trigger for the spell as a formatted string (optional)\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/pf2e/QuteTrait.md",
    "content": "# QuteTrait\n\nPf2eTools Trait attributes (`trait2md.txt`)\n\nExtension of [Pf2eQuteBase](Pf2eQuteBase.md)\n\n## Attributes\n\n[altNames](#altnames), [books](#books), [categories](#categories), [getAliases](#getaliases), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath)\n\n### altNames\n\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### categories\n\nList of categories to which this trait belongs\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### name\n\nNote name\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../Reprinted.md))\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/pf2e/QuteTraitIndex.md",
    "content": "# QuteTraitIndex\n\nPf2eTools Trait index attributes (`indexTrait.md`)\n\nThis replaces the index usually generated for folders.\nThe default template for the trait consructs a list of links to\ntraits grouped by category.\n\nExtension of [Pf2eQuteNote](Pf2eQuteNote.md)\n\n## Attributes\n\n[books](#books), [categoryLinks](#categorylinks), [categoryToTraits](#categorytotraits), [getAliases](#getaliases), [hasSections](#hassections), [labeledSource](#labeledsource), [name](#name), [reprintOf](#reprintof), [source](#source), [sourceAndPage](#sourceandpage), [sourcesWithFootnote](#sourceswithfootnote), [tags](#tags), [text](#text), [vaultPath](#vaultpath)\n\n### books\n\nList of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n\n### categoryLinks\n\nList of category anchor links\n\n### categoryToTraits\n\nMap of category to a list of traits\n\n### getAliases\n\nAliases for this note, including the note name, as quoted/escaped strings.\n\nExample values:\n- \"+1 All-Purpose Tool\"\n- \"Carl \\\"The Elder\\\" Frost\"\n\nIn templates:\n```md\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n```\n\n### hasSections\n\nTrue if the content (text) contains sections\n\n### labeledSource\n\nFormatted string describing the content's source(s): `_Source: <sources>_`\n\n### name\n\nNote name\n\n### reprintOf\n\nList of content superceded by this note (as [Reprinted](../Reprinted.md))\n\n### source\n\nString describing the content's source(s)\n\n### sourceAndPage\n\nBook sources as list of [SourceAndPage](../SourceAndPage.md)\n\n### sourcesWithFootnote\n\nGet Sources as a footnote.\n\nCalling this method will return an italicised string with the primary source\nfollowed by a footnote listing all other sources. Useful for types\nthat tend to have many sources.\n\n### tags\n\nCollected tags for inclusion in frontmatter\n\n### text\n\nFormatted text. For most templates, this is the bulk of the content.\n\n### vaultPath\n\nPath to this note in the vault\n"
  },
  {
    "path": "docs/templates/pf2e/README.md",
    "content": "# Pf2eTools templates\n\nQute templates for generating content from Pf2eTools data.\n\nPathfinder data uses a lot of inline and nested embedding,\nwhich creates additional template variants and some special\nbehavior.\n\n## References\n\n- [Pf2eQuteBase](Pf2eQuteBase.md): Attributes for notes that are generated from the Pf2eTools data.\n- [Pf2eQuteNote](Pf2eQuteNote.md): Attributes for notes that are generated from the Pf2eTools data.\n- [QuteAbility](QuteAbility.md): Pf2eTools Ability attributes (`ability2md.txt` or `inline-ability2md.txt`).\n- [QuteAction](QuteAction/README.md): Pf2eTools Action attributes (`action2md.txt`)\n\n    Extension of [Pf2eQuteBase](Pf2eQuteBase.md)\n\n- [QuteAffliction](QuteAffliction/README.md): Pf2eTools Affliction attributes (inline/embedded, `inline-affliction2md.txt`)\n\n    Extension of [Pf2eQuteNote](Pf2eQuteNote.md)\n\n- [QuteArchetype](QuteArchetype.md): Pf2eTools Archetype attributes (`archetype2md.txt`)\n\n    Extension of [Pf2eQuteBase](Pf2eQuteBase.md)\n\n- [QuteBackground](QuteBackground.md): Pf2eTools Background attributes (`background2md.txt`)\n\n    Extension of [Pf2eQuteBase](Pf2eQuteBase.md)\n\n- [QuteBook](QuteBook/README.md): Pf2eTools Book attributes (`book2md.txt`)\n\n    Extension of [Pf2eQuteNote](Pf2eQuteNote.md)\n\n- [QuteCreature](QuteCreature/README.md): Pf2eTools Creature attributes (`creature2md.txt`)\n\n    Extension of [Pf2eQuteBase](Pf2eQuteBase.md)\n\n- [QuteDataActivity](QuteDataActivity.md): Pf2eTools activity attributes.\n- [QuteDataArmorClass](QuteDataArmorClass.md): Pf2eTools armor class attributes.\n- [QuteDataDefenses](QuteDataDefenses/README.md): Pf2eTools Armor class, Saving Throws, and other attributes describing defenses of a creature or hazard.\n- [QuteDataFrequency](QuteDataFrequency.md): A description of a frequency e.g.\n- [QuteDataHpHardnessBt](QuteDataHpHardnessBt/README.md): Hit Points, Hardness, and a broken threshold for hazards and shields.\n- [QuteDataRange](QuteDataRange/README.md): A range with a given value and unit of measurement for that value.\n- [QuteDataSpeed](QuteDataSpeed.md): Examples:\n\n    - `10 feet, swim 20 feet (some note); some ability`\n    - `10 feet, swim 20 feet, some ability`\n\n- [QuteDataTimedDuration](QuteDataTimedDuration/README.md): A duration of time, represented by a numerical value and a unit.\n- [QuteDeity](QuteDeity/README.md): Pf2eTools Deity attributes (`deity2md.txt`)\n\n    Deities are rendered both standalone and inline (as an admonition block).\n\n- [QuteFeat](QuteFeat.md): Pf2eTools Feat attributes (`feat2md.txt`)\n\n    Feats are rendered both standalone and inline (as an admonition block).\n\n- [QuteHazard](QuteHazard/README.md): Pf2eTools Hazard attributes (`hazard2md.txt`)\n\n    Hazards are rendered both standalone and inline (as an admonition block).\n\n- [QuteInlineAttack](QuteInlineAttack/README.md): Pf2eTools Attack attributes (inline/embedded, `inline-attack2md.txt`)\n\n    When used directly, renders according to `inline-attack2md.txt`\n\n- [QuteItem](QuteItem/README.md): Pf2eTools Item attributes\n\n    Extension of [Pf2eQuteBase](Pf2eQuteBase.md)\n\n- [QuteRitual](QuteRitual/README.md): Pf2eTools Ritual attributes (`ritual2md.txt`)\n\n    Extension of [Pf2eQuteBase](Pf2eQuteBase.md)\n\n- [QuteSpell](QuteSpell/README.md): Pf2eTools Spell attributes (`spell2md.txt`)\n\n    Extension of [Pf2eQuteBase](Pf2eQuteBase.md)\n\n- [QuteTrait](QuteTrait.md): Pf2eTools Trait attributes (`trait2md.txt`)\n\n    Extension of [Pf2eQuteBase](Pf2eQuteBase.md)\n\n- [QuteTraitIndex](QuteTraitIndex.md): Pf2eTools Trait index attributes (`indexTrait.md`)\n\n    This replaces the index usually generated for folders.\n"
  },
  {
    "path": "examples/README.md",
    "content": "# Examples\n\n- [Admonition types](admonitions)\n- [CSS Snippets](css-snippets)\n- [Templates](templates)\n"
  },
  {
    "path": "examples/admonitions/README.md",
    "content": "# Admonitions\n\nInstall and enable the `Admonition` plugin from the Community Plugins pane in Obsidian.\n\nImport one or more of the following admonition json files to create the custom admonition types used for converted content:\n\n- [**admonitions-5e.json**](admonitions-5e.json) for 5e tools: `flowchart`, `gallery`, `readaloud`, `statblock`\n- [**admonitions-pf2e-v3.json**](admonitions-pf2e-v3.json) for pf2e tools: `embed-ability`, `embed-action`, `embed-avatar`, `embed-disease`, `embed-feat`, `embed-item`, `embed-ritual`, `inline-affliction`, `inline-attack`, `pf2-note`, `pf2-summary`, `statblock-pf2e`\n- [**other-admonitions.json**](other-admonitions.json) if they are interesting: `charm`, `letter`, `npc`, `scene`, `skill`, `weather`\n"
  },
  {
    "path": "examples/admonitions/admonitions-5e.json",
    "content": "[\n  {\n    \"type\": \"statblock\",\n    \"icon\": {\n      \"name\": \"dragon\",\n      \"type\": \"font-awesome\"\n    },\n    \"command\": false,\n    \"injectColor\": false,\n    \"noTitle\": false,\n    \"copy\": false\n  },\n  {\n    \"type\": \"flowchart\",\n    \"icon\": {\n      \"name\": \"map\",\n      \"type\": \"font-awesome\"\n    },\n    \"command\": false,\n    \"injectColor\": false,\n    \"noTitle\": true,\n    \"copy\": false\n  },\n  {\n    \"type\": \"gallery\",\n    \"icon\": {\n      \"name\": \"image\",\n      \"type\": \"font-awesome\"\n    },\n    \"command\": false,\n    \"injectColor\": false,\n    \"noTitle\": true,\n    \"copy\": false\n  },\n  {\n    \"type\": \"readaloud\",\n    \"icon\": {\n      \"name\": \"book-reader\",\n      \"type\": \"font-awesome\"\n    },\n    \"title\": \"Read Aloud\",\n    \"command\": false,\n    \"injectColor\": false,\n    \"noTitle\": false,\n    \"copy\": false\n  }\n]\n"
  },
  {
    "path": "examples/admonitions/admonitions-pf2e-v3.json",
    "content": "[\n  {\n    \"type\": \"embed-feat\",\n    \"color\": \"239, 187, 189\",\n    \"icon\": {\n      \"name\": \"fort-awesome-alt\",\n      \"type\": \"font-awesome\"\n    },\n    \"command\": false,\n    \"injectColor\": false,\n    \"noTitle\": false,\n    \"copy\": true\n  },\n  {\n    \"type\": \"embed-ability\",\n    \"color\": \"182, 197, 200\",\n    \"icon\": {\n      \"type\": \"obsidian\",\n      \"name\": \"lucide-swords\"\n    },\n    \"command\": false,\n    \"injectColor\": false,\n    \"noTitle\": false,\n    \"copy\": true\n  },\n  {\n    \"type\": \"inline-affliction\",\n    \"color\": \"217, 216, 218\",\n    \"icon\": {\n      \"name\": \"virus-slash\",\n      \"type\": \"font-awesome\"\n    },\n    \"command\": false,\n    \"injectColor\": true,\n    \"noTitle\": true,\n    \"copy\": true\n  },\n  {\n    \"type\": \"embed-avatar\",\n    \"color\": \"236, 202, 99\",\n    \"icon\": {\n      \"name\": \"superpowers\",\n      \"type\": \"font-awesome\"\n    },\n    \"command\": false,\n    \"injectColor\": false,\n    \"noTitle\": false,\n    \"copy\": true\n  },\n  {\n    \"type\": \"embed-action\",\n    \"color\": \"206, 206, 197\",\n    \"icon\": {\n      \"type\": \"obsidian\",\n      \"name\": \"lucide-hammer\"\n    },\n    \"command\": false,\n    \"injectColor\": false,\n    \"noTitle\": false,\n    \"copy\": true\n  },\n  {\n    \"type\": \"embed-disease\",\n    \"color\": \"36, 36, 36\",\n    \"icon\": {\n      \"name\": \"head-side-virus\",\n      \"type\": \"font-awesome\"\n    },\n    \"command\": false,\n    \"injectColor\": false,\n    \"noTitle\": false,\n    \"copy\": true\n  },\n  {\n    \"type\": \"embed-item\",\n    \"color\": \"163, 200, 209\",\n    \"icon\": {\n      \"type\": \"obsidian\",\n      \"name\": \"lucide-coins\"\n    },\n    \"command\": false,\n    \"injectColor\": false,\n    \"noTitle\": false,\n    \"copy\": true\n  },\n  {\n    \"type\": \"embed-ritual\",\n    \"color\": \"158, 187, 144\",\n    \"icon\": {\n      \"name\": \"circle-notch\",\n      \"type\": \"font-awesome\"\n    },\n    \"command\": false,\n    \"injectColor\": false,\n    \"noTitle\": false,\n    \"copy\": true\n  },\n  {\n    \"type\": \"inline-attack\",\n    \"color\": \"233, 195, 99\",\n    \"icon\": {\n      \"name\": \"fire-alt\",\n      \"type\": \"font-awesome\"\n    },\n    \"command\": false,\n    \"injectColor\": true,\n    \"noTitle\": true,\n    \"copy\": true\n  },\n  {\n    \"type\": \"pf2-note\",\n    \"color\": \"227, 223, 187\",\n    \"icon\": {\n      \"name\": \"scroll\",\n      \"type\": \"font-awesome\"\n    },\n    \"command\": false,\n    \"injectColor\": false,\n    \"noTitle\": false,\n    \"copy\": true\n  },\n  {\n    \"type\": \"pf2-summary\",\n    \"color\": \"209, 224, 224\",\n    \"icon\": {\n      \"name\": \"book-open\",\n      \"type\": \"font-awesome\"\n    },\n    \"command\": false,\n    \"injectColor\": false,\n    \"noTitle\": false,\n    \"copy\": true\n  },\n  {\n    \"type\": \"statblock-pf2e\",\n    \"color\": \"196, 153, 33\",\n    \"icon\": {\n      \"name\": \"dragon\",\n      \"type\": \"font-awesome\"\n    },\n    \"command\": false,\n    \"injectColor\": true,\n    \"noTitle\": true,\n    \"copy\": false\n  }\n]\n"
  },
  {
    "path": "examples/admonitions/other-admonitions.json",
    "content": "[\n  {\n    \"type\": \"charm\",\n    \"icon\": {\n      \"name\": \"magic\",\n      \"type\": \"font-awesome\"\n    },\n    \"title\": \"Charm\",\n    \"command\": false,\n    \"injectColor\": false,\n    \"noTitle\": false,\n    \"copy\": false\n  },\n  {\n    \"type\": \"letter\",\n    \"icon\": {\n      \"name\": \"envelope-open-text\",\n      \"type\": \"font-awesome\"\n    },\n    \"title\": \"Letter\",\n    \"command\": false,\n    \"injectColor\": false,\n    \"noTitle\": false,\n    \"copy\": false\n  },\n  {\n    \"type\": \"npc\",\n    \"icon\": {\n      \"name\": \"address-card\",\n      \"type\": \"font-awesome\"\n    },\n    \"title\": \"NPC\",\n    \"command\": false,\n    \"injectColor\": false,\n    \"noTitle\": false,\n    \"copy\": false\n  },\n  {\n    \"type\": \"scene\",\n    \"icon\": {\n      \"name\": \"archway\",\n      \"type\": \"font-awesome\"\n    },\n    \"title\": \"Scene\",\n    \"command\": false,\n    \"injectColor\": false,\n    \"noTitle\": false,\n    \"copy\": false\n  },\n  {\n    \"type\": \"skill\",\n    \"icon\": {\n      \"name\": \"hand-sparkles\",\n      \"type\": \"font-awesome\"\n    },\n    \"title\": \"Skill Check\",\n    \"command\": false,\n    \"injectColor\": false,\n    \"noTitle\": false,\n    \"copy\": false\n  },\n  {\n    \"type\": \"weather\",\n    \"icon\": {\n      \"name\": \"cloud-sun\",\n      \"type\": \"font-awesome\"\n    },\n    \"title\": \"Weather\",\n    \"command\": false,\n    \"injectColor\": false,\n    \"noTitle\": false,\n    \"copy\": false\n  }\n]\n"
  },
  {
    "path": "examples/config/README.md",
    "content": "# Example configuration files\n\n> [!NOTE]\n> ttrpg-convert-cli supports both JSON and YAML config files. Examples of both file types are provided below.\n>\n> 📝 JSON and YAML are both file formats for storing data in useful and human-readable ways.\n>\n> - JSON: If you want to know why the `{}` and `[]` are used in the ways that they are, you can read about json *objects* and *arrays* [here](https://www.toolsqa.com/rest-assured/what-is-json/)).\n> - YAML (yet another markup language/\"YAML Ain't Markup Language\") is described by a [specification](https://yaml.org/spec/1.2/spec.html). Leading whitespace (proper indentation) is crucial in YAML documents.\n\n- **5eTools config examples**: [config.5e.json](config.5e.json), [config.5e.yaml](config.5e.yaml)\n- **Pf2eTools config examples**: [config.pf2e.json](config.pf2e.json), [config.pf2e.yaml](config.pf2e.yaml)\n\n> [!NOTE]\n> See [configuration.md](../../docs/configuration.md) for more information on setting up config files.\n"
  },
  {
    "path": "examples/config/config.5e.json",
    "content": "{\n  \"sources\" : {\n    \"toolsRoot\" : \"local/5etools/data\",\n    \"reference\" : [\n      \"DMG\"\n    ],\n    \"adventure\" : [\n      \"LMoP\"\n    ],\n    \"book\" : [\n      \"PHB\"\n    ],\n    \"homebrew\" : [\n      \"homebrew/collection/Kobold Press; Deep Magic 14 Elemental Magic.json\"\n    ]\n  },\n  \"paths\" : {\n    \"compendium\" : \"/compendium/\",\n    \"rules\" : \"/compendium/rules/\"\n  },\n  \"include\" : [\n    \"race|changeling|mpmm\"\n  ],\n  \"includeGroup\" : [\n    \"familiars\"\n  ],\n  \"exclude\" : [\n    \"monster|expert|dc\",\n    \"monster|expert|sdw\",\n    \"monster|expert|slw\"\n  ],\n  \"excludePattern\" : [\n    \"race\\\\|.*\\\\|dmg\"\n  ],\n  \"reprintBehavior\" : \"newest\",\n  \"template\" : {\n    \"background\" : \"examples/templates/tools5e/images-background2md.txt\"\n  },\n  \"useDiceRoller\" : true,\n  \"yamlStatblocks\" : true,\n  \"tagPrefix\" : \"ttrpg-cli\",\n  \"images\" : {\n    \"internalRoot\" : \"local/path/for/remote/images\",\n    \"copyInternal\" : true,\n    \"copyExternal\" : true\n  }\n}"
  },
  {
    "path": "examples/config/config.5e.yaml",
    "content": "---\nsources:\n    toolsRoot: \"local/5etools/data\"\n    reference:\n        - \"DMG\"\n    adventure:\n        - \"LMoP\"\n    book:\n        - \"PHB\"\n    homebrew:\n        - \"homebrew/collection/Kobold Press; Deep Magic 14 Elemental Magic.json\"\npaths:\n    compendium: \"/compendium/\"\n    rules: \"/compendium/rules/\"\ninclude:\n    - \"race|changeling|mpmm\"\nincludeGroup:\n    - \"familiars\"\nexclude:\n    - \"monster|expert|dc\"\n    - \"monster|expert|sdw\"\n    - \"monster|expert|slw\"\nexcludePattern:\n    - \"race\\\\|.*\\\\|dmg\"\nreprintBehavior: \"newest\"\ntemplate:\n    background: \"examples/templates/tools5e/images-background2md.txt\"\nuseDiceRoller: true\nyamlStatblocks: true\ntagPrefix: \"ttrpg-cli\"\nimages:\n    internalRoot: \"local/path/for/remote/images\"\n    copyInternal: true\n    copyExternal: true\n"
  },
  {
    "path": "examples/config/config.pf2e.json",
    "content": "{\n  \"sources\" : {\n    \"reference\" : [\n      \"CRB\",\n      \"GMG\"\n    ],\n    \"book\" : [\n      \"crb\",\n      \"gmg\"\n    ]\n  },\n  \"paths\" : {\n    \"compendium\" : \"compendium/\",\n    \"rules\" : \"compendium/rules/\"\n  },\n  \"include\" : [\n    \"ability|buck|b1\"\n  ],\n  \"exclude\" : [\n    \"background|insurgent|apg\"\n  ],\n  \"excludePattern\" : [\n    \"background\\\\|.*\\\\|lowg\"\n  ],\n  \"reprintBehavior\" : \"newest\",\n  \"template\" : {\n    \"ability\" : \"../path/to/ability2md.txt\"\n  },\n  \"useDiceRoller\" : true,\n  \"tagPrefix\" : \"ttrpg-cli\",\n  \"images\" : { }\n}"
  },
  {
    "path": "examples/config/config.pf2e.yaml",
    "content": "---\nsources:\n    reference:\n        - \"CRB\"\n        - \"GMG\"\n    book:\n        - \"crb\"\n        - \"gmg\"\npaths:\n    compendium: \"compendium/\"\n    rules: \"compendium/rules/\"\ninclude:\n    - \"ability|buck|b1\"\nexclude:\n    - \"background|insurgent|apg\"\nexcludePattern:\n    - \"background\\\\|.*\\\\|lowg\"\nreprintBehavior: \"newest\"\ntemplate:\n    ability: \"../path/to/ability2md.txt\"\nuseDiceRoller: true\ntagPrefix: \"ttrpg-cli\"\nimages: {\n    }\n"
  },
  {
    "path": "examples/config/config.schema.json",
    "content": "{\n  \"$schema\" : \"https://json-schema.org/draft/2020-12/schema\",\n  \"$defs\" : {\n    \"Map(String,String)\" : {\n      \"type\" : \"object\"\n    }\n  },\n  \"type\" : \"object\",\n  \"properties\" : {\n    \"exclude\" : {\n      \"type\" : \"array\",\n      \"items\" : {\n        \"type\" : \"string\"\n      }\n    },\n    \"excludePattern\" : {\n      \"type\" : \"array\",\n      \"items\" : {\n        \"type\" : \"string\"\n      }\n    },\n    \"from\" : {\n      \"type\" : \"array\",\n      \"items\" : {\n        \"type\" : \"string\"\n      }\n    },\n    \"images\" : {\n      \"type\" : \"object\",\n      \"properties\" : {\n        \"copyExternal\" : {\n          \"type\" : \"boolean\"\n        },\n        \"copyInternal\" : {\n          \"type\" : \"boolean\"\n        },\n        \"fallbackPaths\" : {\n          \"$ref\" : \"#/$defs/Map(String,String)\"\n        },\n        \"internalRoot\" : {\n          \"type\" : \"string\"\n        }\n      }\n    },\n    \"include\" : {\n      \"type\" : \"array\",\n      \"items\" : {\n        \"type\" : \"string\"\n      }\n    },\n    \"includeGroup\" : {\n      \"type\" : \"array\",\n      \"items\" : {\n        \"type\" : \"string\"\n      }\n    },\n    \"includePattern\" : {\n      \"type\" : \"array\",\n      \"items\" : {\n        \"type\" : \"string\"\n      }\n    },\n    \"onlyReferencedTables\" : {\n      \"type\" : \"boolean\"\n    },\n    \"paths\" : {\n      \"type\" : \"object\",\n      \"properties\" : {\n        \"compendium\" : {\n          \"type\" : \"string\"\n        },\n        \"rules\" : {\n          \"type\" : \"string\"\n        }\n      }\n    },\n    \"racesAsSpecies\" : {\n      \"type\" : \"boolean\"\n    },\n    \"reprintBehavior\" : {\n      \"type\" : \"string\",\n      \"enum\" : [ \"newest\", \"edition\", \"all\" ]\n    },\n    \"sources\" : {\n      \"type\" : \"object\",\n      \"properties\" : {\n        \"adventure\" : {\n          \"type\" : \"array\",\n          \"items\" : {\n            \"type\" : \"string\"\n          }\n        },\n        \"book\" : {\n          \"type\" : \"array\",\n          \"items\" : {\n            \"type\" : \"string\"\n          }\n        },\n        \"defaultSource\" : {\n          \"$ref\" : \"#/$defs/Map(String,String)\"\n        },\n        \"homebrew\" : {\n          \"type\" : \"array\",\n          \"items\" : {\n            \"type\" : \"string\"\n          }\n        },\n        \"reference\" : {\n          \"type\" : \"array\",\n          \"items\" : {\n            \"type\" : \"string\"\n          }\n        },\n        \"toolsRoot\" : {\n          \"type\" : \"string\"\n        }\n      }\n    },\n    \"splitRules\" : {\n      \"type\" : \"boolean\"\n    },\n    \"tagPrefix\" : {\n      \"type\" : \"string\"\n    },\n    \"template\" : {\n      \"$ref\" : \"#/$defs/Map(String,String)\"\n    },\n    \"useDiceRoller\" : {\n      \"type\" : \"boolean\"\n    },\n    \"yamlStatblocks\" : {\n      \"type\" : \"boolean\"\n    }\n  }\n}"
  },
  {
    "path": "examples/css-snippets/README.md",
    "content": "# Optional CSS Snippets\n\nThe following CSS snippets have been created to further customize the look of the generated content.\n\n- **5eTools**\n    - [dnd5e-only-admonitions.css](dnd5e-only-admonitions.css) - styles for 5e admonitions (charm, gallery, flowchart, letter, npc, scene, skill, weather)\n    - [dnd5e-float-images.css](dnd5e-float-images.css) - formatted image types using anchor tags\n        - `#card` (used with decks): float right; width: 150px\n        - `#center`: centered; max-height: 60% of height\n        - `#portrait`: float right; width: 200px\n        - `#right`: float right; max-height: 60% of height; max-width: 50%\n        - `#symbol` (deities): float right; width: 200px\n        - `#token` (statblocks): float right; width: 150px\n    - [dnd5e-only-statblock.css](dnd5e-only-statblock.css) - statblock with floating token image (only)\n    - [dnd5e-compendium.css](dnd5e-compendium.css) - admonitions + statblock + float images (center, card, symbol, token, right, portrait)\n- **PF2eTools**\n    - [pf2-only-statblocks.css](pf2-only-statblocks.css) - more realistic looking Statblocks; Action and Trait styles\n    - [pf2-compendium.css](pf2-compendium.css) - Statblocks; Action and Trait styles; Paizo colors, text formatting, etc.\n- **Other**\n    - [hide-markdown-link-url](hide-markdown-link-url.css) - hide url portion of markdown links in source mode\n\n## Styles for Fantasy Statblocks\n\nCompendium (`*-compendium`) snippets include styles for statblocks.\n\nIf you aren't using a `*-compendium` snippet, you may want to download either `dnd5e-only-statblocks.css` or `pf2-only-statblocks.css` to style your statblocks.\n\n> [!CAUTION]\n> ⚠️ Do not use an `*-only-statblock.css` snippet and a `*-compendium.css` snippet together.\n"
  },
  {
    "path": "examples/css-snippets/dnd5e-compendium.css",
    "content": "@charset \"UTF-8\";\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/dnd5e-compendium.scss */\nbody {\n  --admonition-charm: 211,141,159;\n  --admonition-charm-text: var(--admonition-charm);\n  --admonition-letter: 98, 159, 197;\n  --admonition-npc: 102, 121, 137;\n  --admonition-scene: 139, 167, 145;\n  --admonition-skill: 236,201,134;\n  --admonition-skill-text: var(--admonition-skill);\n  --admonition-weather: 53,119,174;\n  --admonition-flowchart: 72,72,72;\n}\n\n.theme-light {\n  --admonition-charm: 222,170,184;\n  --admonition-charm-text: 167,92,112;\n  --admonition-npc: 58, 125, 127;\n  --admonition-scene: 92, 122, 99;\n  --admonition-skill: 221,178,84;\n  --admonition-skill-text: 157,101,83;\n}\n\n.callout[data-callout=charm] {\n  --callout-color: var(--admonition-charm);\n  --callout-title-color: rgb(var(--admonition-charm-text));\n}\n.callout[data-callout=charm] .callout-title {\n  color: var(--callout-title-color);\n}\n\n.callout[data-callout=letter] {\n  --callout-color: var(--admonition-letter);\n}\n\n.callout[data-callout=npc] {\n  --callout-color: var(--admonition-npc);\n}\n\n.callout[data-callout=readaloud],\n.callout[data-callout=scene] {\n  --callout-color: var(--admonition-scene);\n}\n\n.callout[data-callout=skill] {\n  --callout-color: var(--admonition-skill);\n  --callout-title-color: rgb(var(--admonition-skill-text));\n}\n\n.callout[data-callout=weather] {\n  --callout-color: var(--admonition-weather);\n}\n\n.callout[data-callout=flowchart] {\n  --callout-color: var(--admonition-flowchart);\n  --callout-border-width: 0.10rem;\n}\n\n.callout[data-callout^=embed-] {\n  --embed-border-left: none;\n  --embed-border-right: none;\n  --embed-padding: 0;\n}\n\n.json5e-background div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]),\n.json5e-class div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]),\n.json5e-deck div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]),\n.json5e-deity div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]),\n.json5e-feat div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]),\n.json5e-hazard div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]),\n.json5e-item div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]),\n.json5e-monster div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]),\n.json5e-note div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]),\n.json5e-object div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]),\n.json5e-psionic div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]),\n.json5e-race div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]),\n.json5e-reward div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]),\n.json5e-spell div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]),\n.json5e-vehicle div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]) {\n  position: relative;\n}\n.json5e-background div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]):after,\n.json5e-class div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]):after,\n.json5e-deck div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]):after,\n.json5e-deity div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]):after,\n.json5e-feat div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]):after,\n.json5e-hazard div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]):after,\n.json5e-item div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]):after,\n.json5e-monster div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]):after,\n.json5e-note div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]):after,\n.json5e-object div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]):after,\n.json5e-psionic div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]):after,\n.json5e-race div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]):after,\n.json5e-reward div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]):after,\n.json5e-spell div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]):after,\n.json5e-vehicle div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]):after {\n  content: \"↓\";\n  color: var(--admonition-flowchart);\n  display: block;\n  position: absolute;\n  bottom: -10px;\n  left: 50%;\n  margin-left: 7px;\n  width: 14px;\n  height: 14px;\n  font-size: 14px;\n  text-align: center;\n}\n.json5e-background .callout[data-callout=gallery],\n.json5e-class .callout[data-callout=gallery],\n.json5e-deck .callout[data-callout=gallery],\n.json5e-deity .callout[data-callout=gallery],\n.json5e-feat .callout[data-callout=gallery],\n.json5e-hazard .callout[data-callout=gallery],\n.json5e-item .callout[data-callout=gallery],\n.json5e-monster .callout[data-callout=gallery],\n.json5e-note .callout[data-callout=gallery],\n.json5e-object .callout[data-callout=gallery],\n.json5e-psionic .callout[data-callout=gallery],\n.json5e-race .callout[data-callout=gallery],\n.json5e-reward .callout[data-callout=gallery],\n.json5e-spell .callout[data-callout=gallery],\n.json5e-vehicle .callout[data-callout=gallery] {\n  --callout-color: transparent;\n  --callout-border-width: 0;\n}\n.json5e-background .callout[data-callout=gallery] .callout-content p,\n.json5e-class .callout[data-callout=gallery] .callout-content p,\n.json5e-deck .callout[data-callout=gallery] .callout-content p,\n.json5e-deity .callout[data-callout=gallery] .callout-content p,\n.json5e-feat .callout[data-callout=gallery] .callout-content p,\n.json5e-hazard .callout[data-callout=gallery] .callout-content p,\n.json5e-item .callout[data-callout=gallery] .callout-content p,\n.json5e-monster .callout[data-callout=gallery] .callout-content p,\n.json5e-note .callout[data-callout=gallery] .callout-content p,\n.json5e-object .callout[data-callout=gallery] .callout-content p,\n.json5e-psionic .callout[data-callout=gallery] .callout-content p,\n.json5e-race .callout[data-callout=gallery] .callout-content p,\n.json5e-reward .callout[data-callout=gallery] .callout-content p,\n.json5e-spell .callout[data-callout=gallery] .callout-content p,\n.json5e-vehicle .callout[data-callout=gallery] .callout-content p {\n  display: flex;\n  flex-wrap: wrap;\n  justify-content: space-evenly;\n  align-content: center;\n}\n.json5e-background .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"],\n.json5e-background .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"],\n.json5e-class .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"],\n.json5e-class .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"],\n.json5e-deck .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"],\n.json5e-deck .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"],\n.json5e-deity .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"],\n.json5e-deity .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"],\n.json5e-feat .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"],\n.json5e-feat .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"],\n.json5e-hazard .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"],\n.json5e-hazard .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"],\n.json5e-item .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"],\n.json5e-item .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"],\n.json5e-monster .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"],\n.json5e-monster .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"],\n.json5e-note .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"],\n.json5e-note .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"],\n.json5e-object .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"],\n.json5e-object .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"],\n.json5e-psionic .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"],\n.json5e-psionic .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"],\n.json5e-race .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"],\n.json5e-race .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"],\n.json5e-reward .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"],\n.json5e-reward .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"],\n.json5e-spell .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"],\n.json5e-spell .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"],\n.json5e-vehicle .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"],\n.json5e-vehicle .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] {\n  max-width: 49%;\n}\n.json5e-background .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"] img,\n.json5e-background .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] img,\n.json5e-class .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"] img,\n.json5e-class .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] img,\n.json5e-deck .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"] img,\n.json5e-deck .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] img,\n.json5e-deity .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"] img,\n.json5e-deity .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] img,\n.json5e-feat .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"] img,\n.json5e-feat .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] img,\n.json5e-hazard .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"] img,\n.json5e-hazard .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] img,\n.json5e-item .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"] img,\n.json5e-item .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] img,\n.json5e-monster .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"] img,\n.json5e-monster .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] img,\n.json5e-note .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"] img,\n.json5e-note .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] img,\n.json5e-object .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"] img,\n.json5e-object .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] img,\n.json5e-psionic .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"] img,\n.json5e-psionic .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] img,\n.json5e-race .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"] img,\n.json5e-race .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] img,\n.json5e-reward .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"] img,\n.json5e-reward .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] img,\n.json5e-spell .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"] img,\n.json5e-spell .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] img,\n.json5e-vehicle .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"] img,\n.json5e-vehicle .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] img {\n  max-height: 40vh;\n}\n.json5e-background .callout[data-callout=gallery] .callout-title,\n.json5e-class .callout[data-callout=gallery] .callout-title,\n.json5e-deck .callout[data-callout=gallery] .callout-title,\n.json5e-deity .callout[data-callout=gallery] .callout-title,\n.json5e-feat .callout[data-callout=gallery] .callout-title,\n.json5e-hazard .callout[data-callout=gallery] .callout-title,\n.json5e-item .callout[data-callout=gallery] .callout-title,\n.json5e-monster .callout[data-callout=gallery] .callout-title,\n.json5e-note .callout[data-callout=gallery] .callout-title,\n.json5e-object .callout[data-callout=gallery] .callout-title,\n.json5e-psionic .callout[data-callout=gallery] .callout-title,\n.json5e-race .callout[data-callout=gallery] .callout-title,\n.json5e-reward .callout[data-callout=gallery] .callout-title,\n.json5e-spell .callout[data-callout=gallery] .callout-title,\n.json5e-vehicle .callout[data-callout=gallery] .callout-title {\n  display: none;\n}\n\nbody {\n  --statblock-accent: 201,60,60;\n}\n\n.admonition-statblock-parent {\n  clear: both;\n}\n\n.callout[data-callout=statblock] {\n  --callout-color: var(--statblock-accent);\n  --embed-border-left: none;\n  --embed-border-right: none;\n  --embed-padding: 0;\n}\n.callout[data-callout=statblock] .callout-title {\n  line-height: var(--line-height);\n}\n.callout[data-callout=statblock] .callout-title .callout-title-content {\n  flex: 2;\n  font-size: var(--h3-size);\n}\n.callout[data-callout=statblock] .callout-content > :first-child {\n  margin-top: 0.5em;\n}\n.callout[data-callout=statblock] .callout-content > :last-child {\n  margin-bottom: 0.5em;\n}\n.callout[data-callout=statblock] .callout-content h1,\n.callout[data-callout=statblock] .callout-content h2,\n.callout[data-callout=statblock] .callout-content h3 {\n  font-family: var(--default-font);\n  font-variant: common-ligatures small-caps;\n}\n.callout[data-callout=statblock] .callout-content h1 {\n  font-size: 1.4em;\n  line-height: 1.4em;\n  margin: 0;\n  padding: 0;\n}\n.callout[data-callout=statblock] .callout-content h2,\n.callout[data-callout=statblock] .callout-content h3 {\n  font-size: 1.2em;\n  line-height: 1.2em;\n  padding: 0.5em 0 0 0;\n  margin-top: 0.2em;\n  margin-bottom: 0.3em;\n  border-bottom: 1px solid rgb(var(--statblock-accent));\n}\n.callout[data-callout=statblock] .callout-content p {\n  line-height: 1.2em;\n  margin-block-start: 0.5em;\n}\n.callout[data-callout=statblock] .callout-content li {\n  line-height: 1.2em;\n  margin-block-start: 0.5em;\n}\n.callout[data-callout=statblock] .markdown-embed .markdown-embed-title,\n.callout[data-callout=statblock] .markdown-embed .mod-header {\n  display: none;\n}\n.callout[data-callout=statblock] .markdown-embed pre.frontmatter,\n.callout[data-callout=statblock] .markdown-embed h1[data-heading] {\n  display: none;\n}\n.callout[data-callout=statblock] .markdown-embed .markdown-embed-content {\n  max-height: unset;\n  overflow: unset;\n}\n.callout[data-callout=statblock] .markdown-embed .markdown-embed-content > .markdown-preview-view {\n  overflow-y: unset;\n}\n\np div.image-embed:hover::after,\np span.image-embed:hover::after,\np img:hover::after,\ndiv div.image-embed:hover::after,\ndiv span.image-embed:hover::after,\ndiv img:hover::after,\nspan div.image-embed:hover::after,\nspan span.image-embed:hover::after,\nspan img:hover::after {\n  content: attr(alt);\n  position: absolute;\n  background: rgba(0, 0, 0, 0.8);\n  color: white;\n  padding: 4px 8px;\n  border-radius: 4px;\n  font-size: 0.8em;\n  width: 200px;\n  max-width: 200px;\n  z-index: 10;\n  top: 2em;\n  left: 50%;\n  transform: translateX(-50%);\n}\n\n.json5e-background.markdown-preview-view div.image-embed,\n.json5e-background.markdown-preview-view span.image-embed,\n.json5e-background.markdown-preview-view span.internal-embed.image-embed,\n.json5e-background.markdown-preview-view img.image-embed, .json5e-background.markdown-source-view div.image-embed,\n.json5e-background.markdown-source-view span.image-embed,\n.json5e-background.markdown-source-view span.internal-embed.image-embed,\n.json5e-background.markdown-source-view img.image-embed,\n.json5e-class.markdown-preview-view div.image-embed,\n.json5e-class.markdown-preview-view span.image-embed,\n.json5e-class.markdown-preview-view span.internal-embed.image-embed,\n.json5e-class.markdown-preview-view img.image-embed,\n.json5e-class.markdown-source-view div.image-embed,\n.json5e-class.markdown-source-view span.image-embed,\n.json5e-class.markdown-source-view span.internal-embed.image-embed,\n.json5e-class.markdown-source-view img.image-embed,\n.json5e-deck.markdown-preview-view div.image-embed,\n.json5e-deck.markdown-preview-view span.image-embed,\n.json5e-deck.markdown-preview-view span.internal-embed.image-embed,\n.json5e-deck.markdown-preview-view img.image-embed,\n.json5e-deck.markdown-source-view div.image-embed,\n.json5e-deck.markdown-source-view span.image-embed,\n.json5e-deck.markdown-source-view span.internal-embed.image-embed,\n.json5e-deck.markdown-source-view img.image-embed,\n.json5e-deity.markdown-preview-view div.image-embed,\n.json5e-deity.markdown-preview-view span.image-embed,\n.json5e-deity.markdown-preview-view span.internal-embed.image-embed,\n.json5e-deity.markdown-preview-view img.image-embed,\n.json5e-deity.markdown-source-view div.image-embed,\n.json5e-deity.markdown-source-view span.image-embed,\n.json5e-deity.markdown-source-view span.internal-embed.image-embed,\n.json5e-deity.markdown-source-view img.image-embed,\n.json5e-feat.markdown-preview-view div.image-embed,\n.json5e-feat.markdown-preview-view span.image-embed,\n.json5e-feat.markdown-preview-view span.internal-embed.image-embed,\n.json5e-feat.markdown-preview-view img.image-embed,\n.json5e-feat.markdown-source-view div.image-embed,\n.json5e-feat.markdown-source-view span.image-embed,\n.json5e-feat.markdown-source-view span.internal-embed.image-embed,\n.json5e-feat.markdown-source-view img.image-embed,\n.json5e-hazard.markdown-preview-view div.image-embed,\n.json5e-hazard.markdown-preview-view span.image-embed,\n.json5e-hazard.markdown-preview-view span.internal-embed.image-embed,\n.json5e-hazard.markdown-preview-view img.image-embed,\n.json5e-hazard.markdown-source-view div.image-embed,\n.json5e-hazard.markdown-source-view span.image-embed,\n.json5e-hazard.markdown-source-view span.internal-embed.image-embed,\n.json5e-hazard.markdown-source-view img.image-embed,\n.json5e-item.markdown-preview-view div.image-embed,\n.json5e-item.markdown-preview-view span.image-embed,\n.json5e-item.markdown-preview-view span.internal-embed.image-embed,\n.json5e-item.markdown-preview-view img.image-embed,\n.json5e-item.markdown-source-view div.image-embed,\n.json5e-item.markdown-source-view span.image-embed,\n.json5e-item.markdown-source-view span.internal-embed.image-embed,\n.json5e-item.markdown-source-view img.image-embed,\n.json5e-monster.markdown-preview-view div.image-embed,\n.json5e-monster.markdown-preview-view span.image-embed,\n.json5e-monster.markdown-preview-view span.internal-embed.image-embed,\n.json5e-monster.markdown-preview-view img.image-embed,\n.json5e-monster.markdown-source-view div.image-embed,\n.json5e-monster.markdown-source-view span.image-embed,\n.json5e-monster.markdown-source-view span.internal-embed.image-embed,\n.json5e-monster.markdown-source-view img.image-embed,\n.json5e-note.markdown-preview-view div.image-embed,\n.json5e-note.markdown-preview-view span.image-embed,\n.json5e-note.markdown-preview-view span.internal-embed.image-embed,\n.json5e-note.markdown-preview-view img.image-embed,\n.json5e-note.markdown-source-view div.image-embed,\n.json5e-note.markdown-source-view span.image-embed,\n.json5e-note.markdown-source-view span.internal-embed.image-embed,\n.json5e-note.markdown-source-view img.image-embed,\n.json5e-object.markdown-preview-view div.image-embed,\n.json5e-object.markdown-preview-view span.image-embed,\n.json5e-object.markdown-preview-view span.internal-embed.image-embed,\n.json5e-object.markdown-preview-view img.image-embed,\n.json5e-object.markdown-source-view div.image-embed,\n.json5e-object.markdown-source-view span.image-embed,\n.json5e-object.markdown-source-view span.internal-embed.image-embed,\n.json5e-object.markdown-source-view img.image-embed,\n.json5e-psionic.markdown-preview-view div.image-embed,\n.json5e-psionic.markdown-preview-view span.image-embed,\n.json5e-psionic.markdown-preview-view span.internal-embed.image-embed,\n.json5e-psionic.markdown-preview-view img.image-embed,\n.json5e-psionic.markdown-source-view div.image-embed,\n.json5e-psionic.markdown-source-view span.image-embed,\n.json5e-psionic.markdown-source-view span.internal-embed.image-embed,\n.json5e-psionic.markdown-source-view img.image-embed,\n.json5e-race.markdown-preview-view div.image-embed,\n.json5e-race.markdown-preview-view span.image-embed,\n.json5e-race.markdown-preview-view span.internal-embed.image-embed,\n.json5e-race.markdown-preview-view img.image-embed,\n.json5e-race.markdown-source-view div.image-embed,\n.json5e-race.markdown-source-view span.image-embed,\n.json5e-race.markdown-source-view span.internal-embed.image-embed,\n.json5e-race.markdown-source-view img.image-embed,\n.json5e-reward.markdown-preview-view div.image-embed,\n.json5e-reward.markdown-preview-view span.image-embed,\n.json5e-reward.markdown-preview-view span.internal-embed.image-embed,\n.json5e-reward.markdown-preview-view img.image-embed,\n.json5e-reward.markdown-source-view div.image-embed,\n.json5e-reward.markdown-source-view span.image-embed,\n.json5e-reward.markdown-source-view span.internal-embed.image-embed,\n.json5e-reward.markdown-source-view img.image-embed,\n.json5e-species.markdown-preview-view div.image-embed,\n.json5e-species.markdown-preview-view span.image-embed,\n.json5e-species.markdown-preview-view span.internal-embed.image-embed,\n.json5e-species.markdown-preview-view img.image-embed,\n.json5e-species.markdown-source-view div.image-embed,\n.json5e-species.markdown-source-view span.image-embed,\n.json5e-species.markdown-source-view span.internal-embed.image-embed,\n.json5e-species.markdown-source-view img.image-embed,\n.json5e-spell.markdown-preview-view div.image-embed,\n.json5e-spell.markdown-preview-view span.image-embed,\n.json5e-spell.markdown-preview-view span.internal-embed.image-embed,\n.json5e-spell.markdown-preview-view img.image-embed,\n.json5e-spell.markdown-source-view div.image-embed,\n.json5e-spell.markdown-source-view span.image-embed,\n.json5e-spell.markdown-source-view span.internal-embed.image-embed,\n.json5e-spell.markdown-source-view img.image-embed,\n.json5e-vehicle.markdown-preview-view div.image-embed,\n.json5e-vehicle.markdown-preview-view span.image-embed,\n.json5e-vehicle.markdown-preview-view span.internal-embed.image-embed,\n.json5e-vehicle.markdown-preview-view img.image-embed,\n.json5e-vehicle.markdown-source-view div.image-embed,\n.json5e-vehicle.markdown-source-view span.image-embed,\n.json5e-vehicle.markdown-source-view span.internal-embed.image-embed,\n.json5e-vehicle.markdown-source-view img.image-embed {\n  position: relative;\n  /* Display the alt text after the image */\n  /*\n  &::after {\n    content: attr(alt);\n    display: block;\n    margin-top: 0.5em;\n    top: 100%; // Positions it below the parent element\n    left: 0;\n  }\n  */\n}\n.json5e-background.markdown-preview-view div[src$=\"#center\"],\n.json5e-background.markdown-preview-view span[src$=\"#center\"],\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-background.markdown-preview-view img[src$=\"#center\"], .json5e-background.markdown-source-view div[src$=\"#center\"],\n.json5e-background.markdown-source-view span[src$=\"#center\"],\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-background.markdown-source-view img[src$=\"#center\"],\n.json5e-class.markdown-preview-view div[src$=\"#center\"],\n.json5e-class.markdown-preview-view span[src$=\"#center\"],\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-class.markdown-preview-view img[src$=\"#center\"],\n.json5e-class.markdown-source-view div[src$=\"#center\"],\n.json5e-class.markdown-source-view span[src$=\"#center\"],\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-class.markdown-source-view img[src$=\"#center\"],\n.json5e-deck.markdown-preview-view div[src$=\"#center\"],\n.json5e-deck.markdown-preview-view span[src$=\"#center\"],\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-deck.markdown-preview-view img[src$=\"#center\"],\n.json5e-deck.markdown-source-view div[src$=\"#center\"],\n.json5e-deck.markdown-source-view span[src$=\"#center\"],\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-deck.markdown-source-view img[src$=\"#center\"],\n.json5e-deity.markdown-preview-view div[src$=\"#center\"],\n.json5e-deity.markdown-preview-view span[src$=\"#center\"],\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-deity.markdown-preview-view img[src$=\"#center\"],\n.json5e-deity.markdown-source-view div[src$=\"#center\"],\n.json5e-deity.markdown-source-view span[src$=\"#center\"],\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-deity.markdown-source-view img[src$=\"#center\"],\n.json5e-feat.markdown-preview-view div[src$=\"#center\"],\n.json5e-feat.markdown-preview-view span[src$=\"#center\"],\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-feat.markdown-preview-view img[src$=\"#center\"],\n.json5e-feat.markdown-source-view div[src$=\"#center\"],\n.json5e-feat.markdown-source-view span[src$=\"#center\"],\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-feat.markdown-source-view img[src$=\"#center\"],\n.json5e-hazard.markdown-preview-view div[src$=\"#center\"],\n.json5e-hazard.markdown-preview-view span[src$=\"#center\"],\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-hazard.markdown-preview-view img[src$=\"#center\"],\n.json5e-hazard.markdown-source-view div[src$=\"#center\"],\n.json5e-hazard.markdown-source-view span[src$=\"#center\"],\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-hazard.markdown-source-view img[src$=\"#center\"],\n.json5e-item.markdown-preview-view div[src$=\"#center\"],\n.json5e-item.markdown-preview-view span[src$=\"#center\"],\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-item.markdown-preview-view img[src$=\"#center\"],\n.json5e-item.markdown-source-view div[src$=\"#center\"],\n.json5e-item.markdown-source-view span[src$=\"#center\"],\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-item.markdown-source-view img[src$=\"#center\"],\n.json5e-monster.markdown-preview-view div[src$=\"#center\"],\n.json5e-monster.markdown-preview-view span[src$=\"#center\"],\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-monster.markdown-preview-view img[src$=\"#center\"],\n.json5e-monster.markdown-source-view div[src$=\"#center\"],\n.json5e-monster.markdown-source-view span[src$=\"#center\"],\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-monster.markdown-source-view img[src$=\"#center\"],\n.json5e-note.markdown-preview-view div[src$=\"#center\"],\n.json5e-note.markdown-preview-view span[src$=\"#center\"],\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-note.markdown-preview-view img[src$=\"#center\"],\n.json5e-note.markdown-source-view div[src$=\"#center\"],\n.json5e-note.markdown-source-view span[src$=\"#center\"],\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-note.markdown-source-view img[src$=\"#center\"],\n.json5e-object.markdown-preview-view div[src$=\"#center\"],\n.json5e-object.markdown-preview-view span[src$=\"#center\"],\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-object.markdown-preview-view img[src$=\"#center\"],\n.json5e-object.markdown-source-view div[src$=\"#center\"],\n.json5e-object.markdown-source-view span[src$=\"#center\"],\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-object.markdown-source-view img[src$=\"#center\"],\n.json5e-psionic.markdown-preview-view div[src$=\"#center\"],\n.json5e-psionic.markdown-preview-view span[src$=\"#center\"],\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-psionic.markdown-preview-view img[src$=\"#center\"],\n.json5e-psionic.markdown-source-view div[src$=\"#center\"],\n.json5e-psionic.markdown-source-view span[src$=\"#center\"],\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-psionic.markdown-source-view img[src$=\"#center\"],\n.json5e-race.markdown-preview-view div[src$=\"#center\"],\n.json5e-race.markdown-preview-view span[src$=\"#center\"],\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-race.markdown-preview-view img[src$=\"#center\"],\n.json5e-race.markdown-source-view div[src$=\"#center\"],\n.json5e-race.markdown-source-view span[src$=\"#center\"],\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-race.markdown-source-view img[src$=\"#center\"],\n.json5e-reward.markdown-preview-view div[src$=\"#center\"],\n.json5e-reward.markdown-preview-view span[src$=\"#center\"],\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-reward.markdown-preview-view img[src$=\"#center\"],\n.json5e-reward.markdown-source-view div[src$=\"#center\"],\n.json5e-reward.markdown-source-view span[src$=\"#center\"],\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-reward.markdown-source-view img[src$=\"#center\"],\n.json5e-species.markdown-preview-view div[src$=\"#center\"],\n.json5e-species.markdown-preview-view span[src$=\"#center\"],\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-species.markdown-preview-view img[src$=\"#center\"],\n.json5e-species.markdown-source-view div[src$=\"#center\"],\n.json5e-species.markdown-source-view span[src$=\"#center\"],\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-species.markdown-source-view img[src$=\"#center\"],\n.json5e-spell.markdown-preview-view div[src$=\"#center\"],\n.json5e-spell.markdown-preview-view span[src$=\"#center\"],\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-spell.markdown-preview-view img[src$=\"#center\"],\n.json5e-spell.markdown-source-view div[src$=\"#center\"],\n.json5e-spell.markdown-source-view span[src$=\"#center\"],\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-spell.markdown-source-view img[src$=\"#center\"],\n.json5e-vehicle.markdown-preview-view div[src$=\"#center\"],\n.json5e-vehicle.markdown-preview-view span[src$=\"#center\"],\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-vehicle.markdown-preview-view img[src$=\"#center\"],\n.json5e-vehicle.markdown-source-view div[src$=\"#center\"],\n.json5e-vehicle.markdown-source-view span[src$=\"#center\"],\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-vehicle.markdown-source-view img[src$=\"#center\"] {\n  display: flex;\n  width: 100%;\n  justify-content: center;\n  align-items: center;\n}\n.json5e-background.markdown-preview-view div[src$=\"#card\"], .json5e-background.markdown-preview-view div[src$=\"#symbol\"], .json5e-background.markdown-preview-view div[src$=\"#portrait\"], .json5e-background.markdown-preview-view div[src$=\"#token\"], .json5e-background.markdown-preview-view div[src$=\"#right\"],\n.json5e-background.markdown-preview-view span[src$=\"#card\"],\n.json5e-background.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-background.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-background.markdown-preview-view span[src$=\"#token\"],\n.json5e-background.markdown-preview-view span[src$=\"#right\"],\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-background.markdown-preview-view img[src$=\"#card\"],\n.json5e-background.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-background.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-background.markdown-preview-view img[src$=\"#token\"],\n.json5e-background.markdown-preview-view img[src$=\"#right\"], .json5e-background.markdown-source-view div[src$=\"#card\"], .json5e-background.markdown-source-view div[src$=\"#symbol\"], .json5e-background.markdown-source-view div[src$=\"#portrait\"], .json5e-background.markdown-source-view div[src$=\"#token\"], .json5e-background.markdown-source-view div[src$=\"#right\"],\n.json5e-background.markdown-source-view span[src$=\"#card\"],\n.json5e-background.markdown-source-view span[src$=\"#symbol\"],\n.json5e-background.markdown-source-view span[src$=\"#portrait\"],\n.json5e-background.markdown-source-view span[src$=\"#token\"],\n.json5e-background.markdown-source-view span[src$=\"#right\"],\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-background.markdown-source-view img[src$=\"#card\"],\n.json5e-background.markdown-source-view img[src$=\"#symbol\"],\n.json5e-background.markdown-source-view img[src$=\"#portrait\"],\n.json5e-background.markdown-source-view img[src$=\"#token\"],\n.json5e-background.markdown-source-view img[src$=\"#right\"],\n.json5e-class.markdown-preview-view div[src$=\"#card\"],\n.json5e-class.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-class.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-class.markdown-preview-view div[src$=\"#token\"],\n.json5e-class.markdown-preview-view div[src$=\"#right\"],\n.json5e-class.markdown-preview-view span[src$=\"#card\"],\n.json5e-class.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-class.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-class.markdown-preview-view span[src$=\"#token\"],\n.json5e-class.markdown-preview-view span[src$=\"#right\"],\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-class.markdown-preview-view img[src$=\"#card\"],\n.json5e-class.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-class.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-class.markdown-preview-view img[src$=\"#token\"],\n.json5e-class.markdown-preview-view img[src$=\"#right\"],\n.json5e-class.markdown-source-view div[src$=\"#card\"],\n.json5e-class.markdown-source-view div[src$=\"#symbol\"],\n.json5e-class.markdown-source-view div[src$=\"#portrait\"],\n.json5e-class.markdown-source-view div[src$=\"#token\"],\n.json5e-class.markdown-source-view div[src$=\"#right\"],\n.json5e-class.markdown-source-view span[src$=\"#card\"],\n.json5e-class.markdown-source-view span[src$=\"#symbol\"],\n.json5e-class.markdown-source-view span[src$=\"#portrait\"],\n.json5e-class.markdown-source-view span[src$=\"#token\"],\n.json5e-class.markdown-source-view span[src$=\"#right\"],\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-class.markdown-source-view img[src$=\"#card\"],\n.json5e-class.markdown-source-view img[src$=\"#symbol\"],\n.json5e-class.markdown-source-view img[src$=\"#portrait\"],\n.json5e-class.markdown-source-view img[src$=\"#token\"],\n.json5e-class.markdown-source-view img[src$=\"#right\"],\n.json5e-deck.markdown-preview-view div[src$=\"#card\"],\n.json5e-deck.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-deck.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-deck.markdown-preview-view div[src$=\"#token\"],\n.json5e-deck.markdown-preview-view div[src$=\"#right\"],\n.json5e-deck.markdown-preview-view span[src$=\"#card\"],\n.json5e-deck.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-deck.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-deck.markdown-preview-view span[src$=\"#token\"],\n.json5e-deck.markdown-preview-view span[src$=\"#right\"],\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-deck.markdown-preview-view img[src$=\"#card\"],\n.json5e-deck.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-deck.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-deck.markdown-preview-view img[src$=\"#token\"],\n.json5e-deck.markdown-preview-view img[src$=\"#right\"],\n.json5e-deck.markdown-source-view div[src$=\"#card\"],\n.json5e-deck.markdown-source-view div[src$=\"#symbol\"],\n.json5e-deck.markdown-source-view div[src$=\"#portrait\"],\n.json5e-deck.markdown-source-view div[src$=\"#token\"],\n.json5e-deck.markdown-source-view div[src$=\"#right\"],\n.json5e-deck.markdown-source-view span[src$=\"#card\"],\n.json5e-deck.markdown-source-view span[src$=\"#symbol\"],\n.json5e-deck.markdown-source-view span[src$=\"#portrait\"],\n.json5e-deck.markdown-source-view span[src$=\"#token\"],\n.json5e-deck.markdown-source-view span[src$=\"#right\"],\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-deck.markdown-source-view img[src$=\"#card\"],\n.json5e-deck.markdown-source-view img[src$=\"#symbol\"],\n.json5e-deck.markdown-source-view img[src$=\"#portrait\"],\n.json5e-deck.markdown-source-view img[src$=\"#token\"],\n.json5e-deck.markdown-source-view img[src$=\"#right\"],\n.json5e-deity.markdown-preview-view div[src$=\"#card\"],\n.json5e-deity.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-deity.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-deity.markdown-preview-view div[src$=\"#token\"],\n.json5e-deity.markdown-preview-view div[src$=\"#right\"],\n.json5e-deity.markdown-preview-view span[src$=\"#card\"],\n.json5e-deity.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-deity.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-deity.markdown-preview-view span[src$=\"#token\"],\n.json5e-deity.markdown-preview-view span[src$=\"#right\"],\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-deity.markdown-preview-view img[src$=\"#card\"],\n.json5e-deity.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-deity.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-deity.markdown-preview-view img[src$=\"#token\"],\n.json5e-deity.markdown-preview-view img[src$=\"#right\"],\n.json5e-deity.markdown-source-view div[src$=\"#card\"],\n.json5e-deity.markdown-source-view div[src$=\"#symbol\"],\n.json5e-deity.markdown-source-view div[src$=\"#portrait\"],\n.json5e-deity.markdown-source-view div[src$=\"#token\"],\n.json5e-deity.markdown-source-view div[src$=\"#right\"],\n.json5e-deity.markdown-source-view span[src$=\"#card\"],\n.json5e-deity.markdown-source-view span[src$=\"#symbol\"],\n.json5e-deity.markdown-source-view span[src$=\"#portrait\"],\n.json5e-deity.markdown-source-view span[src$=\"#token\"],\n.json5e-deity.markdown-source-view span[src$=\"#right\"],\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-deity.markdown-source-view img[src$=\"#card\"],\n.json5e-deity.markdown-source-view img[src$=\"#symbol\"],\n.json5e-deity.markdown-source-view img[src$=\"#portrait\"],\n.json5e-deity.markdown-source-view img[src$=\"#token\"],\n.json5e-deity.markdown-source-view img[src$=\"#right\"],\n.json5e-feat.markdown-preview-view div[src$=\"#card\"],\n.json5e-feat.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-feat.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-feat.markdown-preview-view div[src$=\"#token\"],\n.json5e-feat.markdown-preview-view div[src$=\"#right\"],\n.json5e-feat.markdown-preview-view span[src$=\"#card\"],\n.json5e-feat.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-feat.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-feat.markdown-preview-view span[src$=\"#token\"],\n.json5e-feat.markdown-preview-view span[src$=\"#right\"],\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-feat.markdown-preview-view img[src$=\"#card\"],\n.json5e-feat.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-feat.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-feat.markdown-preview-view img[src$=\"#token\"],\n.json5e-feat.markdown-preview-view img[src$=\"#right\"],\n.json5e-feat.markdown-source-view div[src$=\"#card\"],\n.json5e-feat.markdown-source-view div[src$=\"#symbol\"],\n.json5e-feat.markdown-source-view div[src$=\"#portrait\"],\n.json5e-feat.markdown-source-view div[src$=\"#token\"],\n.json5e-feat.markdown-source-view div[src$=\"#right\"],\n.json5e-feat.markdown-source-view span[src$=\"#card\"],\n.json5e-feat.markdown-source-view span[src$=\"#symbol\"],\n.json5e-feat.markdown-source-view span[src$=\"#portrait\"],\n.json5e-feat.markdown-source-view span[src$=\"#token\"],\n.json5e-feat.markdown-source-view span[src$=\"#right\"],\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-feat.markdown-source-view img[src$=\"#card\"],\n.json5e-feat.markdown-source-view img[src$=\"#symbol\"],\n.json5e-feat.markdown-source-view img[src$=\"#portrait\"],\n.json5e-feat.markdown-source-view img[src$=\"#token\"],\n.json5e-feat.markdown-source-view img[src$=\"#right\"],\n.json5e-hazard.markdown-preview-view div[src$=\"#card\"],\n.json5e-hazard.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-hazard.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-hazard.markdown-preview-view div[src$=\"#token\"],\n.json5e-hazard.markdown-preview-view div[src$=\"#right\"],\n.json5e-hazard.markdown-preview-view span[src$=\"#card\"],\n.json5e-hazard.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-hazard.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-hazard.markdown-preview-view span[src$=\"#token\"],\n.json5e-hazard.markdown-preview-view span[src$=\"#right\"],\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-hazard.markdown-preview-view img[src$=\"#card\"],\n.json5e-hazard.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-hazard.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-hazard.markdown-preview-view img[src$=\"#token\"],\n.json5e-hazard.markdown-preview-view img[src$=\"#right\"],\n.json5e-hazard.markdown-source-view div[src$=\"#card\"],\n.json5e-hazard.markdown-source-view div[src$=\"#symbol\"],\n.json5e-hazard.markdown-source-view div[src$=\"#portrait\"],\n.json5e-hazard.markdown-source-view div[src$=\"#token\"],\n.json5e-hazard.markdown-source-view div[src$=\"#right\"],\n.json5e-hazard.markdown-source-view span[src$=\"#card\"],\n.json5e-hazard.markdown-source-view span[src$=\"#symbol\"],\n.json5e-hazard.markdown-source-view span[src$=\"#portrait\"],\n.json5e-hazard.markdown-source-view span[src$=\"#token\"],\n.json5e-hazard.markdown-source-view span[src$=\"#right\"],\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-hazard.markdown-source-view img[src$=\"#card\"],\n.json5e-hazard.markdown-source-view img[src$=\"#symbol\"],\n.json5e-hazard.markdown-source-view img[src$=\"#portrait\"],\n.json5e-hazard.markdown-source-view img[src$=\"#token\"],\n.json5e-hazard.markdown-source-view img[src$=\"#right\"],\n.json5e-item.markdown-preview-view div[src$=\"#card\"],\n.json5e-item.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-item.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-item.markdown-preview-view div[src$=\"#token\"],\n.json5e-item.markdown-preview-view div[src$=\"#right\"],\n.json5e-item.markdown-preview-view span[src$=\"#card\"],\n.json5e-item.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-item.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-item.markdown-preview-view span[src$=\"#token\"],\n.json5e-item.markdown-preview-view span[src$=\"#right\"],\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-item.markdown-preview-view img[src$=\"#card\"],\n.json5e-item.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-item.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-item.markdown-preview-view img[src$=\"#token\"],\n.json5e-item.markdown-preview-view img[src$=\"#right\"],\n.json5e-item.markdown-source-view div[src$=\"#card\"],\n.json5e-item.markdown-source-view div[src$=\"#symbol\"],\n.json5e-item.markdown-source-view div[src$=\"#portrait\"],\n.json5e-item.markdown-source-view div[src$=\"#token\"],\n.json5e-item.markdown-source-view div[src$=\"#right\"],\n.json5e-item.markdown-source-view span[src$=\"#card\"],\n.json5e-item.markdown-source-view span[src$=\"#symbol\"],\n.json5e-item.markdown-source-view span[src$=\"#portrait\"],\n.json5e-item.markdown-source-view span[src$=\"#token\"],\n.json5e-item.markdown-source-view span[src$=\"#right\"],\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-item.markdown-source-view img[src$=\"#card\"],\n.json5e-item.markdown-source-view img[src$=\"#symbol\"],\n.json5e-item.markdown-source-view img[src$=\"#portrait\"],\n.json5e-item.markdown-source-view img[src$=\"#token\"],\n.json5e-item.markdown-source-view img[src$=\"#right\"],\n.json5e-monster.markdown-preview-view div[src$=\"#card\"],\n.json5e-monster.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-monster.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-monster.markdown-preview-view div[src$=\"#token\"],\n.json5e-monster.markdown-preview-view div[src$=\"#right\"],\n.json5e-monster.markdown-preview-view span[src$=\"#card\"],\n.json5e-monster.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-monster.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-monster.markdown-preview-view span[src$=\"#token\"],\n.json5e-monster.markdown-preview-view span[src$=\"#right\"],\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-monster.markdown-preview-view img[src$=\"#card\"],\n.json5e-monster.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-monster.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-monster.markdown-preview-view img[src$=\"#token\"],\n.json5e-monster.markdown-preview-view img[src$=\"#right\"],\n.json5e-monster.markdown-source-view div[src$=\"#card\"],\n.json5e-monster.markdown-source-view div[src$=\"#symbol\"],\n.json5e-monster.markdown-source-view div[src$=\"#portrait\"],\n.json5e-monster.markdown-source-view div[src$=\"#token\"],\n.json5e-monster.markdown-source-view div[src$=\"#right\"],\n.json5e-monster.markdown-source-view span[src$=\"#card\"],\n.json5e-monster.markdown-source-view span[src$=\"#symbol\"],\n.json5e-monster.markdown-source-view span[src$=\"#portrait\"],\n.json5e-monster.markdown-source-view span[src$=\"#token\"],\n.json5e-monster.markdown-source-view span[src$=\"#right\"],\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-monster.markdown-source-view img[src$=\"#card\"],\n.json5e-monster.markdown-source-view img[src$=\"#symbol\"],\n.json5e-monster.markdown-source-view img[src$=\"#portrait\"],\n.json5e-monster.markdown-source-view img[src$=\"#token\"],\n.json5e-monster.markdown-source-view img[src$=\"#right\"],\n.json5e-note.markdown-preview-view div[src$=\"#card\"],\n.json5e-note.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-note.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-note.markdown-preview-view div[src$=\"#token\"],\n.json5e-note.markdown-preview-view div[src$=\"#right\"],\n.json5e-note.markdown-preview-view span[src$=\"#card\"],\n.json5e-note.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-note.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-note.markdown-preview-view span[src$=\"#token\"],\n.json5e-note.markdown-preview-view span[src$=\"#right\"],\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-note.markdown-preview-view img[src$=\"#card\"],\n.json5e-note.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-note.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-note.markdown-preview-view img[src$=\"#token\"],\n.json5e-note.markdown-preview-view img[src$=\"#right\"],\n.json5e-note.markdown-source-view div[src$=\"#card\"],\n.json5e-note.markdown-source-view div[src$=\"#symbol\"],\n.json5e-note.markdown-source-view div[src$=\"#portrait\"],\n.json5e-note.markdown-source-view div[src$=\"#token\"],\n.json5e-note.markdown-source-view div[src$=\"#right\"],\n.json5e-note.markdown-source-view span[src$=\"#card\"],\n.json5e-note.markdown-source-view span[src$=\"#symbol\"],\n.json5e-note.markdown-source-view span[src$=\"#portrait\"],\n.json5e-note.markdown-source-view span[src$=\"#token\"],\n.json5e-note.markdown-source-view span[src$=\"#right\"],\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-note.markdown-source-view img[src$=\"#card\"],\n.json5e-note.markdown-source-view img[src$=\"#symbol\"],\n.json5e-note.markdown-source-view img[src$=\"#portrait\"],\n.json5e-note.markdown-source-view img[src$=\"#token\"],\n.json5e-note.markdown-source-view img[src$=\"#right\"],\n.json5e-object.markdown-preview-view div[src$=\"#card\"],\n.json5e-object.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-object.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-object.markdown-preview-view div[src$=\"#token\"],\n.json5e-object.markdown-preview-view div[src$=\"#right\"],\n.json5e-object.markdown-preview-view span[src$=\"#card\"],\n.json5e-object.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-object.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-object.markdown-preview-view span[src$=\"#token\"],\n.json5e-object.markdown-preview-view span[src$=\"#right\"],\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-object.markdown-preview-view img[src$=\"#card\"],\n.json5e-object.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-object.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-object.markdown-preview-view img[src$=\"#token\"],\n.json5e-object.markdown-preview-view img[src$=\"#right\"],\n.json5e-object.markdown-source-view div[src$=\"#card\"],\n.json5e-object.markdown-source-view div[src$=\"#symbol\"],\n.json5e-object.markdown-source-view div[src$=\"#portrait\"],\n.json5e-object.markdown-source-view div[src$=\"#token\"],\n.json5e-object.markdown-source-view div[src$=\"#right\"],\n.json5e-object.markdown-source-view span[src$=\"#card\"],\n.json5e-object.markdown-source-view span[src$=\"#symbol\"],\n.json5e-object.markdown-source-view span[src$=\"#portrait\"],\n.json5e-object.markdown-source-view span[src$=\"#token\"],\n.json5e-object.markdown-source-view span[src$=\"#right\"],\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-object.markdown-source-view img[src$=\"#card\"],\n.json5e-object.markdown-source-view img[src$=\"#symbol\"],\n.json5e-object.markdown-source-view img[src$=\"#portrait\"],\n.json5e-object.markdown-source-view img[src$=\"#token\"],\n.json5e-object.markdown-source-view img[src$=\"#right\"],\n.json5e-psionic.markdown-preview-view div[src$=\"#card\"],\n.json5e-psionic.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-psionic.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-psionic.markdown-preview-view div[src$=\"#token\"],\n.json5e-psionic.markdown-preview-view div[src$=\"#right\"],\n.json5e-psionic.markdown-preview-view span[src$=\"#card\"],\n.json5e-psionic.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-psionic.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-psionic.markdown-preview-view span[src$=\"#token\"],\n.json5e-psionic.markdown-preview-view span[src$=\"#right\"],\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-psionic.markdown-preview-view img[src$=\"#card\"],\n.json5e-psionic.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-psionic.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-psionic.markdown-preview-view img[src$=\"#token\"],\n.json5e-psionic.markdown-preview-view img[src$=\"#right\"],\n.json5e-psionic.markdown-source-view div[src$=\"#card\"],\n.json5e-psionic.markdown-source-view div[src$=\"#symbol\"],\n.json5e-psionic.markdown-source-view div[src$=\"#portrait\"],\n.json5e-psionic.markdown-source-view div[src$=\"#token\"],\n.json5e-psionic.markdown-source-view div[src$=\"#right\"],\n.json5e-psionic.markdown-source-view span[src$=\"#card\"],\n.json5e-psionic.markdown-source-view span[src$=\"#symbol\"],\n.json5e-psionic.markdown-source-view span[src$=\"#portrait\"],\n.json5e-psionic.markdown-source-view span[src$=\"#token\"],\n.json5e-psionic.markdown-source-view span[src$=\"#right\"],\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-psionic.markdown-source-view img[src$=\"#card\"],\n.json5e-psionic.markdown-source-view img[src$=\"#symbol\"],\n.json5e-psionic.markdown-source-view img[src$=\"#portrait\"],\n.json5e-psionic.markdown-source-view img[src$=\"#token\"],\n.json5e-psionic.markdown-source-view img[src$=\"#right\"],\n.json5e-race.markdown-preview-view div[src$=\"#card\"],\n.json5e-race.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-race.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-race.markdown-preview-view div[src$=\"#token\"],\n.json5e-race.markdown-preview-view div[src$=\"#right\"],\n.json5e-race.markdown-preview-view span[src$=\"#card\"],\n.json5e-race.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-race.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-race.markdown-preview-view span[src$=\"#token\"],\n.json5e-race.markdown-preview-view span[src$=\"#right\"],\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-race.markdown-preview-view img[src$=\"#card\"],\n.json5e-race.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-race.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-race.markdown-preview-view img[src$=\"#token\"],\n.json5e-race.markdown-preview-view img[src$=\"#right\"],\n.json5e-race.markdown-source-view div[src$=\"#card\"],\n.json5e-race.markdown-source-view div[src$=\"#symbol\"],\n.json5e-race.markdown-source-view div[src$=\"#portrait\"],\n.json5e-race.markdown-source-view div[src$=\"#token\"],\n.json5e-race.markdown-source-view div[src$=\"#right\"],\n.json5e-race.markdown-source-view span[src$=\"#card\"],\n.json5e-race.markdown-source-view span[src$=\"#symbol\"],\n.json5e-race.markdown-source-view span[src$=\"#portrait\"],\n.json5e-race.markdown-source-view span[src$=\"#token\"],\n.json5e-race.markdown-source-view span[src$=\"#right\"],\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-race.markdown-source-view img[src$=\"#card\"],\n.json5e-race.markdown-source-view img[src$=\"#symbol\"],\n.json5e-race.markdown-source-view img[src$=\"#portrait\"],\n.json5e-race.markdown-source-view img[src$=\"#token\"],\n.json5e-race.markdown-source-view img[src$=\"#right\"],\n.json5e-reward.markdown-preview-view div[src$=\"#card\"],\n.json5e-reward.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-reward.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-reward.markdown-preview-view div[src$=\"#token\"],\n.json5e-reward.markdown-preview-view div[src$=\"#right\"],\n.json5e-reward.markdown-preview-view span[src$=\"#card\"],\n.json5e-reward.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-reward.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-reward.markdown-preview-view span[src$=\"#token\"],\n.json5e-reward.markdown-preview-view span[src$=\"#right\"],\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-reward.markdown-preview-view img[src$=\"#card\"],\n.json5e-reward.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-reward.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-reward.markdown-preview-view img[src$=\"#token\"],\n.json5e-reward.markdown-preview-view img[src$=\"#right\"],\n.json5e-reward.markdown-source-view div[src$=\"#card\"],\n.json5e-reward.markdown-source-view div[src$=\"#symbol\"],\n.json5e-reward.markdown-source-view div[src$=\"#portrait\"],\n.json5e-reward.markdown-source-view div[src$=\"#token\"],\n.json5e-reward.markdown-source-view div[src$=\"#right\"],\n.json5e-reward.markdown-source-view span[src$=\"#card\"],\n.json5e-reward.markdown-source-view span[src$=\"#symbol\"],\n.json5e-reward.markdown-source-view span[src$=\"#portrait\"],\n.json5e-reward.markdown-source-view span[src$=\"#token\"],\n.json5e-reward.markdown-source-view span[src$=\"#right\"],\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-reward.markdown-source-view img[src$=\"#card\"],\n.json5e-reward.markdown-source-view img[src$=\"#symbol\"],\n.json5e-reward.markdown-source-view img[src$=\"#portrait\"],\n.json5e-reward.markdown-source-view img[src$=\"#token\"],\n.json5e-reward.markdown-source-view img[src$=\"#right\"],\n.json5e-species.markdown-preview-view div[src$=\"#card\"],\n.json5e-species.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-species.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-species.markdown-preview-view div[src$=\"#token\"],\n.json5e-species.markdown-preview-view div[src$=\"#right\"],\n.json5e-species.markdown-preview-view span[src$=\"#card\"],\n.json5e-species.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-species.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-species.markdown-preview-view span[src$=\"#token\"],\n.json5e-species.markdown-preview-view span[src$=\"#right\"],\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-species.markdown-preview-view img[src$=\"#card\"],\n.json5e-species.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-species.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-species.markdown-preview-view img[src$=\"#token\"],\n.json5e-species.markdown-preview-view img[src$=\"#right\"],\n.json5e-species.markdown-source-view div[src$=\"#card\"],\n.json5e-species.markdown-source-view div[src$=\"#symbol\"],\n.json5e-species.markdown-source-view div[src$=\"#portrait\"],\n.json5e-species.markdown-source-view div[src$=\"#token\"],\n.json5e-species.markdown-source-view div[src$=\"#right\"],\n.json5e-species.markdown-source-view span[src$=\"#card\"],\n.json5e-species.markdown-source-view span[src$=\"#symbol\"],\n.json5e-species.markdown-source-view span[src$=\"#portrait\"],\n.json5e-species.markdown-source-view span[src$=\"#token\"],\n.json5e-species.markdown-source-view span[src$=\"#right\"],\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-species.markdown-source-view img[src$=\"#card\"],\n.json5e-species.markdown-source-view img[src$=\"#symbol\"],\n.json5e-species.markdown-source-view img[src$=\"#portrait\"],\n.json5e-species.markdown-source-view img[src$=\"#token\"],\n.json5e-species.markdown-source-view img[src$=\"#right\"],\n.json5e-spell.markdown-preview-view div[src$=\"#card\"],\n.json5e-spell.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-spell.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-spell.markdown-preview-view div[src$=\"#token\"],\n.json5e-spell.markdown-preview-view div[src$=\"#right\"],\n.json5e-spell.markdown-preview-view span[src$=\"#card\"],\n.json5e-spell.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-spell.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-spell.markdown-preview-view span[src$=\"#token\"],\n.json5e-spell.markdown-preview-view span[src$=\"#right\"],\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-spell.markdown-preview-view img[src$=\"#card\"],\n.json5e-spell.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-spell.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-spell.markdown-preview-view img[src$=\"#token\"],\n.json5e-spell.markdown-preview-view img[src$=\"#right\"],\n.json5e-spell.markdown-source-view div[src$=\"#card\"],\n.json5e-spell.markdown-source-view div[src$=\"#symbol\"],\n.json5e-spell.markdown-source-view div[src$=\"#portrait\"],\n.json5e-spell.markdown-source-view div[src$=\"#token\"],\n.json5e-spell.markdown-source-view div[src$=\"#right\"],\n.json5e-spell.markdown-source-view span[src$=\"#card\"],\n.json5e-spell.markdown-source-view span[src$=\"#symbol\"],\n.json5e-spell.markdown-source-view span[src$=\"#portrait\"],\n.json5e-spell.markdown-source-view span[src$=\"#token\"],\n.json5e-spell.markdown-source-view span[src$=\"#right\"],\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-spell.markdown-source-view img[src$=\"#card\"],\n.json5e-spell.markdown-source-view img[src$=\"#symbol\"],\n.json5e-spell.markdown-source-view img[src$=\"#portrait\"],\n.json5e-spell.markdown-source-view img[src$=\"#token\"],\n.json5e-spell.markdown-source-view img[src$=\"#right\"],\n.json5e-vehicle.markdown-preview-view div[src$=\"#card\"],\n.json5e-vehicle.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-vehicle.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-vehicle.markdown-preview-view div[src$=\"#token\"],\n.json5e-vehicle.markdown-preview-view div[src$=\"#right\"],\n.json5e-vehicle.markdown-preview-view span[src$=\"#card\"],\n.json5e-vehicle.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-vehicle.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-vehicle.markdown-preview-view span[src$=\"#token\"],\n.json5e-vehicle.markdown-preview-view span[src$=\"#right\"],\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-vehicle.markdown-preview-view img[src$=\"#card\"],\n.json5e-vehicle.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-vehicle.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-vehicle.markdown-preview-view img[src$=\"#token\"],\n.json5e-vehicle.markdown-preview-view img[src$=\"#right\"],\n.json5e-vehicle.markdown-source-view div[src$=\"#card\"],\n.json5e-vehicle.markdown-source-view div[src$=\"#symbol\"],\n.json5e-vehicle.markdown-source-view div[src$=\"#portrait\"],\n.json5e-vehicle.markdown-source-view div[src$=\"#token\"],\n.json5e-vehicle.markdown-source-view div[src$=\"#right\"],\n.json5e-vehicle.markdown-source-view span[src$=\"#card\"],\n.json5e-vehicle.markdown-source-view span[src$=\"#symbol\"],\n.json5e-vehicle.markdown-source-view span[src$=\"#portrait\"],\n.json5e-vehicle.markdown-source-view span[src$=\"#token\"],\n.json5e-vehicle.markdown-source-view span[src$=\"#right\"],\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-vehicle.markdown-source-view img[src$=\"#card\"],\n.json5e-vehicle.markdown-source-view img[src$=\"#symbol\"],\n.json5e-vehicle.markdown-source-view img[src$=\"#portrait\"],\n.json5e-vehicle.markdown-source-view img[src$=\"#token\"],\n.json5e-vehicle.markdown-source-view img[src$=\"#right\"] {\n  float: right;\n  padding-left: 5px;\n}\n.json5e-background.markdown-preview-view div[src$=\"#left\"],\n.json5e-background.markdown-preview-view span[src$=\"#left\"],\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-background.markdown-preview-view img[src$=\"#left\"], .json5e-background.markdown-source-view div[src$=\"#left\"],\n.json5e-background.markdown-source-view span[src$=\"#left\"],\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-background.markdown-source-view img[src$=\"#left\"],\n.json5e-class.markdown-preview-view div[src$=\"#left\"],\n.json5e-class.markdown-preview-view span[src$=\"#left\"],\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-class.markdown-preview-view img[src$=\"#left\"],\n.json5e-class.markdown-source-view div[src$=\"#left\"],\n.json5e-class.markdown-source-view span[src$=\"#left\"],\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-class.markdown-source-view img[src$=\"#left\"],\n.json5e-deck.markdown-preview-view div[src$=\"#left\"],\n.json5e-deck.markdown-preview-view span[src$=\"#left\"],\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-deck.markdown-preview-view img[src$=\"#left\"],\n.json5e-deck.markdown-source-view div[src$=\"#left\"],\n.json5e-deck.markdown-source-view span[src$=\"#left\"],\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-deck.markdown-source-view img[src$=\"#left\"],\n.json5e-deity.markdown-preview-view div[src$=\"#left\"],\n.json5e-deity.markdown-preview-view span[src$=\"#left\"],\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-deity.markdown-preview-view img[src$=\"#left\"],\n.json5e-deity.markdown-source-view div[src$=\"#left\"],\n.json5e-deity.markdown-source-view span[src$=\"#left\"],\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-deity.markdown-source-view img[src$=\"#left\"],\n.json5e-feat.markdown-preview-view div[src$=\"#left\"],\n.json5e-feat.markdown-preview-view span[src$=\"#left\"],\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-feat.markdown-preview-view img[src$=\"#left\"],\n.json5e-feat.markdown-source-view div[src$=\"#left\"],\n.json5e-feat.markdown-source-view span[src$=\"#left\"],\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-feat.markdown-source-view img[src$=\"#left\"],\n.json5e-hazard.markdown-preview-view div[src$=\"#left\"],\n.json5e-hazard.markdown-preview-view span[src$=\"#left\"],\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-hazard.markdown-preview-view img[src$=\"#left\"],\n.json5e-hazard.markdown-source-view div[src$=\"#left\"],\n.json5e-hazard.markdown-source-view span[src$=\"#left\"],\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-hazard.markdown-source-view img[src$=\"#left\"],\n.json5e-item.markdown-preview-view div[src$=\"#left\"],\n.json5e-item.markdown-preview-view span[src$=\"#left\"],\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-item.markdown-preview-view img[src$=\"#left\"],\n.json5e-item.markdown-source-view div[src$=\"#left\"],\n.json5e-item.markdown-source-view span[src$=\"#left\"],\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-item.markdown-source-view img[src$=\"#left\"],\n.json5e-monster.markdown-preview-view div[src$=\"#left\"],\n.json5e-monster.markdown-preview-view span[src$=\"#left\"],\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-monster.markdown-preview-view img[src$=\"#left\"],\n.json5e-monster.markdown-source-view div[src$=\"#left\"],\n.json5e-monster.markdown-source-view span[src$=\"#left\"],\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-monster.markdown-source-view img[src$=\"#left\"],\n.json5e-note.markdown-preview-view div[src$=\"#left\"],\n.json5e-note.markdown-preview-view span[src$=\"#left\"],\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-note.markdown-preview-view img[src$=\"#left\"],\n.json5e-note.markdown-source-view div[src$=\"#left\"],\n.json5e-note.markdown-source-view span[src$=\"#left\"],\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-note.markdown-source-view img[src$=\"#left\"],\n.json5e-object.markdown-preview-view div[src$=\"#left\"],\n.json5e-object.markdown-preview-view span[src$=\"#left\"],\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-object.markdown-preview-view img[src$=\"#left\"],\n.json5e-object.markdown-source-view div[src$=\"#left\"],\n.json5e-object.markdown-source-view span[src$=\"#left\"],\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-object.markdown-source-view img[src$=\"#left\"],\n.json5e-psionic.markdown-preview-view div[src$=\"#left\"],\n.json5e-psionic.markdown-preview-view span[src$=\"#left\"],\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-psionic.markdown-preview-view img[src$=\"#left\"],\n.json5e-psionic.markdown-source-view div[src$=\"#left\"],\n.json5e-psionic.markdown-source-view span[src$=\"#left\"],\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-psionic.markdown-source-view img[src$=\"#left\"],\n.json5e-race.markdown-preview-view div[src$=\"#left\"],\n.json5e-race.markdown-preview-view span[src$=\"#left\"],\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-race.markdown-preview-view img[src$=\"#left\"],\n.json5e-race.markdown-source-view div[src$=\"#left\"],\n.json5e-race.markdown-source-view span[src$=\"#left\"],\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-race.markdown-source-view img[src$=\"#left\"],\n.json5e-reward.markdown-preview-view div[src$=\"#left\"],\n.json5e-reward.markdown-preview-view span[src$=\"#left\"],\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-reward.markdown-preview-view img[src$=\"#left\"],\n.json5e-reward.markdown-source-view div[src$=\"#left\"],\n.json5e-reward.markdown-source-view span[src$=\"#left\"],\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-reward.markdown-source-view img[src$=\"#left\"],\n.json5e-species.markdown-preview-view div[src$=\"#left\"],\n.json5e-species.markdown-preview-view span[src$=\"#left\"],\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-species.markdown-preview-view img[src$=\"#left\"],\n.json5e-species.markdown-source-view div[src$=\"#left\"],\n.json5e-species.markdown-source-view span[src$=\"#left\"],\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-species.markdown-source-view img[src$=\"#left\"],\n.json5e-spell.markdown-preview-view div[src$=\"#left\"],\n.json5e-spell.markdown-preview-view span[src$=\"#left\"],\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-spell.markdown-preview-view img[src$=\"#left\"],\n.json5e-spell.markdown-source-view div[src$=\"#left\"],\n.json5e-spell.markdown-source-view span[src$=\"#left\"],\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-spell.markdown-source-view img[src$=\"#left\"],\n.json5e-vehicle.markdown-preview-view div[src$=\"#left\"],\n.json5e-vehicle.markdown-preview-view span[src$=\"#left\"],\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-vehicle.markdown-preview-view img[src$=\"#left\"],\n.json5e-vehicle.markdown-source-view div[src$=\"#left\"],\n.json5e-vehicle.markdown-source-view span[src$=\"#left\"],\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-vehicle.markdown-source-view img[src$=\"#left\"] {\n  float: left;\n  padding-right: 5px;\n}\n.json5e-background.markdown-preview-view div[src$=\"#left\"] img, .json5e-background.markdown-preview-view div[src$=\"#center\"] img, .json5e-background.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-background.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-background.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-background.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-background.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-background.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-background.markdown-preview-view img[src$=\"#right\"] img, .json5e-background.markdown-source-view div[src$=\"#left\"] img, .json5e-background.markdown-source-view div[src$=\"#center\"] img, .json5e-background.markdown-source-view div[src$=\"#right\"] img,\n.json5e-background.markdown-source-view span[src$=\"#left\"] img,\n.json5e-background.markdown-source-view span[src$=\"#center\"] img,\n.json5e-background.markdown-source-view span[src$=\"#right\"] img,\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-background.markdown-source-view img[src$=\"#left\"] img,\n.json5e-background.markdown-source-view img[src$=\"#center\"] img,\n.json5e-background.markdown-source-view img[src$=\"#right\"] img,\n.json5e-class.markdown-preview-view div[src$=\"#left\"] img,\n.json5e-class.markdown-preview-view div[src$=\"#center\"] img,\n.json5e-class.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-class.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-class.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-class.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-class.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-class.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-class.markdown-preview-view img[src$=\"#right\"] img,\n.json5e-class.markdown-source-view div[src$=\"#left\"] img,\n.json5e-class.markdown-source-view div[src$=\"#center\"] img,\n.json5e-class.markdown-source-view div[src$=\"#right\"] img,\n.json5e-class.markdown-source-view span[src$=\"#left\"] img,\n.json5e-class.markdown-source-view span[src$=\"#center\"] img,\n.json5e-class.markdown-source-view span[src$=\"#right\"] img,\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-class.markdown-source-view img[src$=\"#left\"] img,\n.json5e-class.markdown-source-view img[src$=\"#center\"] img,\n.json5e-class.markdown-source-view img[src$=\"#right\"] img,\n.json5e-deck.markdown-preview-view div[src$=\"#left\"] img,\n.json5e-deck.markdown-preview-view div[src$=\"#center\"] img,\n.json5e-deck.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-deck.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-deck.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-deck.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-deck.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-deck.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-deck.markdown-preview-view img[src$=\"#right\"] img,\n.json5e-deck.markdown-source-view div[src$=\"#left\"] img,\n.json5e-deck.markdown-source-view div[src$=\"#center\"] img,\n.json5e-deck.markdown-source-view div[src$=\"#right\"] img,\n.json5e-deck.markdown-source-view span[src$=\"#left\"] img,\n.json5e-deck.markdown-source-view span[src$=\"#center\"] img,\n.json5e-deck.markdown-source-view span[src$=\"#right\"] img,\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-deck.markdown-source-view img[src$=\"#left\"] img,\n.json5e-deck.markdown-source-view img[src$=\"#center\"] img,\n.json5e-deck.markdown-source-view img[src$=\"#right\"] img,\n.json5e-deity.markdown-preview-view div[src$=\"#left\"] img,\n.json5e-deity.markdown-preview-view div[src$=\"#center\"] img,\n.json5e-deity.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-deity.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-deity.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-deity.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-deity.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-deity.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-deity.markdown-preview-view img[src$=\"#right\"] img,\n.json5e-deity.markdown-source-view div[src$=\"#left\"] img,\n.json5e-deity.markdown-source-view div[src$=\"#center\"] img,\n.json5e-deity.markdown-source-view div[src$=\"#right\"] img,\n.json5e-deity.markdown-source-view span[src$=\"#left\"] img,\n.json5e-deity.markdown-source-view span[src$=\"#center\"] img,\n.json5e-deity.markdown-source-view span[src$=\"#right\"] img,\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-deity.markdown-source-view img[src$=\"#left\"] img,\n.json5e-deity.markdown-source-view img[src$=\"#center\"] img,\n.json5e-deity.markdown-source-view img[src$=\"#right\"] img,\n.json5e-feat.markdown-preview-view div[src$=\"#left\"] img,\n.json5e-feat.markdown-preview-view div[src$=\"#center\"] img,\n.json5e-feat.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-feat.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-feat.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-feat.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-feat.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-feat.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-feat.markdown-preview-view img[src$=\"#right\"] img,\n.json5e-feat.markdown-source-view div[src$=\"#left\"] img,\n.json5e-feat.markdown-source-view div[src$=\"#center\"] img,\n.json5e-feat.markdown-source-view div[src$=\"#right\"] img,\n.json5e-feat.markdown-source-view span[src$=\"#left\"] img,\n.json5e-feat.markdown-source-view span[src$=\"#center\"] img,\n.json5e-feat.markdown-source-view span[src$=\"#right\"] img,\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-feat.markdown-source-view img[src$=\"#left\"] img,\n.json5e-feat.markdown-source-view img[src$=\"#center\"] img,\n.json5e-feat.markdown-source-view img[src$=\"#right\"] img,\n.json5e-hazard.markdown-preview-view div[src$=\"#left\"] img,\n.json5e-hazard.markdown-preview-view div[src$=\"#center\"] img,\n.json5e-hazard.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-hazard.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-hazard.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-hazard.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-hazard.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-hazard.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-hazard.markdown-preview-view img[src$=\"#right\"] img,\n.json5e-hazard.markdown-source-view div[src$=\"#left\"] img,\n.json5e-hazard.markdown-source-view div[src$=\"#center\"] img,\n.json5e-hazard.markdown-source-view div[src$=\"#right\"] img,\n.json5e-hazard.markdown-source-view span[src$=\"#left\"] img,\n.json5e-hazard.markdown-source-view span[src$=\"#center\"] img,\n.json5e-hazard.markdown-source-view span[src$=\"#right\"] img,\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-hazard.markdown-source-view img[src$=\"#left\"] img,\n.json5e-hazard.markdown-source-view img[src$=\"#center\"] img,\n.json5e-hazard.markdown-source-view img[src$=\"#right\"] img,\n.json5e-item.markdown-preview-view div[src$=\"#left\"] img,\n.json5e-item.markdown-preview-view div[src$=\"#center\"] img,\n.json5e-item.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-item.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-item.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-item.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-item.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-item.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-item.markdown-preview-view img[src$=\"#right\"] img,\n.json5e-item.markdown-source-view div[src$=\"#left\"] img,\n.json5e-item.markdown-source-view div[src$=\"#center\"] img,\n.json5e-item.markdown-source-view div[src$=\"#right\"] img,\n.json5e-item.markdown-source-view span[src$=\"#left\"] img,\n.json5e-item.markdown-source-view span[src$=\"#center\"] img,\n.json5e-item.markdown-source-view span[src$=\"#right\"] img,\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-item.markdown-source-view img[src$=\"#left\"] img,\n.json5e-item.markdown-source-view img[src$=\"#center\"] img,\n.json5e-item.markdown-source-view img[src$=\"#right\"] img,\n.json5e-monster.markdown-preview-view div[src$=\"#left\"] img,\n.json5e-monster.markdown-preview-view div[src$=\"#center\"] img,\n.json5e-monster.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-monster.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-monster.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-monster.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-monster.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-monster.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-monster.markdown-preview-view img[src$=\"#right\"] img,\n.json5e-monster.markdown-source-view div[src$=\"#left\"] img,\n.json5e-monster.markdown-source-view div[src$=\"#center\"] img,\n.json5e-monster.markdown-source-view div[src$=\"#right\"] img,\n.json5e-monster.markdown-source-view span[src$=\"#left\"] img,\n.json5e-monster.markdown-source-view span[src$=\"#center\"] img,\n.json5e-monster.markdown-source-view span[src$=\"#right\"] img,\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-monster.markdown-source-view img[src$=\"#left\"] img,\n.json5e-monster.markdown-source-view img[src$=\"#center\"] img,\n.json5e-monster.markdown-source-view img[src$=\"#right\"] img,\n.json5e-note.markdown-preview-view div[src$=\"#left\"] img,\n.json5e-note.markdown-preview-view div[src$=\"#center\"] img,\n.json5e-note.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-note.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-note.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-note.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-note.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-note.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-note.markdown-preview-view img[src$=\"#right\"] img,\n.json5e-note.markdown-source-view div[src$=\"#left\"] img,\n.json5e-note.markdown-source-view div[src$=\"#center\"] img,\n.json5e-note.markdown-source-view div[src$=\"#right\"] img,\n.json5e-note.markdown-source-view span[src$=\"#left\"] img,\n.json5e-note.markdown-source-view span[src$=\"#center\"] img,\n.json5e-note.markdown-source-view span[src$=\"#right\"] img,\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-note.markdown-source-view img[src$=\"#left\"] img,\n.json5e-note.markdown-source-view img[src$=\"#center\"] img,\n.json5e-note.markdown-source-view img[src$=\"#right\"] img,\n.json5e-object.markdown-preview-view div[src$=\"#left\"] img,\n.json5e-object.markdown-preview-view div[src$=\"#center\"] img,\n.json5e-object.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-object.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-object.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-object.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-object.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-object.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-object.markdown-preview-view img[src$=\"#right\"] img,\n.json5e-object.markdown-source-view div[src$=\"#left\"] img,\n.json5e-object.markdown-source-view div[src$=\"#center\"] img,\n.json5e-object.markdown-source-view div[src$=\"#right\"] img,\n.json5e-object.markdown-source-view span[src$=\"#left\"] img,\n.json5e-object.markdown-source-view span[src$=\"#center\"] img,\n.json5e-object.markdown-source-view span[src$=\"#right\"] img,\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-object.markdown-source-view img[src$=\"#left\"] img,\n.json5e-object.markdown-source-view img[src$=\"#center\"] img,\n.json5e-object.markdown-source-view img[src$=\"#right\"] img,\n.json5e-psionic.markdown-preview-view div[src$=\"#left\"] img,\n.json5e-psionic.markdown-preview-view div[src$=\"#center\"] img,\n.json5e-psionic.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-psionic.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-psionic.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-psionic.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-psionic.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-psionic.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-psionic.markdown-preview-view img[src$=\"#right\"] img,\n.json5e-psionic.markdown-source-view div[src$=\"#left\"] img,\n.json5e-psionic.markdown-source-view div[src$=\"#center\"] img,\n.json5e-psionic.markdown-source-view div[src$=\"#right\"] img,\n.json5e-psionic.markdown-source-view span[src$=\"#left\"] img,\n.json5e-psionic.markdown-source-view span[src$=\"#center\"] img,\n.json5e-psionic.markdown-source-view span[src$=\"#right\"] img,\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-psionic.markdown-source-view img[src$=\"#left\"] img,\n.json5e-psionic.markdown-source-view img[src$=\"#center\"] img,\n.json5e-psionic.markdown-source-view img[src$=\"#right\"] img,\n.json5e-race.markdown-preview-view div[src$=\"#left\"] img,\n.json5e-race.markdown-preview-view div[src$=\"#center\"] img,\n.json5e-race.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-race.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-race.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-race.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-race.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-race.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-race.markdown-preview-view img[src$=\"#right\"] img,\n.json5e-race.markdown-source-view div[src$=\"#left\"] img,\n.json5e-race.markdown-source-view div[src$=\"#center\"] img,\n.json5e-race.markdown-source-view div[src$=\"#right\"] img,\n.json5e-race.markdown-source-view span[src$=\"#left\"] img,\n.json5e-race.markdown-source-view span[src$=\"#center\"] img,\n.json5e-race.markdown-source-view span[src$=\"#right\"] img,\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-race.markdown-source-view img[src$=\"#left\"] img,\n.json5e-race.markdown-source-view img[src$=\"#center\"] img,\n.json5e-race.markdown-source-view img[src$=\"#right\"] img,\n.json5e-reward.markdown-preview-view div[src$=\"#left\"] img,\n.json5e-reward.markdown-preview-view div[src$=\"#center\"] img,\n.json5e-reward.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-reward.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-reward.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-reward.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-reward.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-reward.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-reward.markdown-preview-view img[src$=\"#right\"] img,\n.json5e-reward.markdown-source-view div[src$=\"#left\"] img,\n.json5e-reward.markdown-source-view div[src$=\"#center\"] img,\n.json5e-reward.markdown-source-view div[src$=\"#right\"] img,\n.json5e-reward.markdown-source-view span[src$=\"#left\"] img,\n.json5e-reward.markdown-source-view span[src$=\"#center\"] img,\n.json5e-reward.markdown-source-view span[src$=\"#right\"] img,\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-reward.markdown-source-view img[src$=\"#left\"] img,\n.json5e-reward.markdown-source-view img[src$=\"#center\"] img,\n.json5e-reward.markdown-source-view img[src$=\"#right\"] img,\n.json5e-species.markdown-preview-view div[src$=\"#left\"] img,\n.json5e-species.markdown-preview-view div[src$=\"#center\"] img,\n.json5e-species.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-species.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-species.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-species.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-species.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-species.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-species.markdown-preview-view img[src$=\"#right\"] img,\n.json5e-species.markdown-source-view div[src$=\"#left\"] img,\n.json5e-species.markdown-source-view div[src$=\"#center\"] img,\n.json5e-species.markdown-source-view div[src$=\"#right\"] img,\n.json5e-species.markdown-source-view span[src$=\"#left\"] img,\n.json5e-species.markdown-source-view span[src$=\"#center\"] img,\n.json5e-species.markdown-source-view span[src$=\"#right\"] img,\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-species.markdown-source-view img[src$=\"#left\"] img,\n.json5e-species.markdown-source-view img[src$=\"#center\"] img,\n.json5e-species.markdown-source-view img[src$=\"#right\"] img,\n.json5e-spell.markdown-preview-view div[src$=\"#left\"] img,\n.json5e-spell.markdown-preview-view div[src$=\"#center\"] img,\n.json5e-spell.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-spell.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-spell.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-spell.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-spell.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-spell.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-spell.markdown-preview-view img[src$=\"#right\"] img,\n.json5e-spell.markdown-source-view div[src$=\"#left\"] img,\n.json5e-spell.markdown-source-view div[src$=\"#center\"] img,\n.json5e-spell.markdown-source-view div[src$=\"#right\"] img,\n.json5e-spell.markdown-source-view span[src$=\"#left\"] img,\n.json5e-spell.markdown-source-view span[src$=\"#center\"] img,\n.json5e-spell.markdown-source-view span[src$=\"#right\"] img,\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-spell.markdown-source-view img[src$=\"#left\"] img,\n.json5e-spell.markdown-source-view img[src$=\"#center\"] img,\n.json5e-spell.markdown-source-view img[src$=\"#right\"] img,\n.json5e-vehicle.markdown-preview-view div[src$=\"#left\"] img,\n.json5e-vehicle.markdown-preview-view div[src$=\"#center\"] img,\n.json5e-vehicle.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-vehicle.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-vehicle.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-vehicle.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-vehicle.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-vehicle.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-vehicle.markdown-preview-view img[src$=\"#right\"] img,\n.json5e-vehicle.markdown-source-view div[src$=\"#left\"] img,\n.json5e-vehicle.markdown-source-view div[src$=\"#center\"] img,\n.json5e-vehicle.markdown-source-view div[src$=\"#right\"] img,\n.json5e-vehicle.markdown-source-view span[src$=\"#left\"] img,\n.json5e-vehicle.markdown-source-view span[src$=\"#center\"] img,\n.json5e-vehicle.markdown-source-view span[src$=\"#right\"] img,\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-vehicle.markdown-source-view img[src$=\"#left\"] img,\n.json5e-vehicle.markdown-source-view img[src$=\"#center\"] img,\n.json5e-vehicle.markdown-source-view img[src$=\"#right\"] img {\n  max-height: 60vh;\n}\n.json5e-background.markdown-preview-view div[src$=\"#left\"], .json5e-background.markdown-preview-view div[src$=\"#right\"],\n.json5e-background.markdown-preview-view span[src$=\"#left\"],\n.json5e-background.markdown-preview-view span[src$=\"#right\"],\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-background.markdown-preview-view img[src$=\"#left\"],\n.json5e-background.markdown-preview-view img[src$=\"#right\"], .json5e-background.markdown-source-view div[src$=\"#left\"], .json5e-background.markdown-source-view div[src$=\"#right\"],\n.json5e-background.markdown-source-view span[src$=\"#left\"],\n.json5e-background.markdown-source-view span[src$=\"#right\"],\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-background.markdown-source-view img[src$=\"#left\"],\n.json5e-background.markdown-source-view img[src$=\"#right\"],\n.json5e-class.markdown-preview-view div[src$=\"#left\"],\n.json5e-class.markdown-preview-view div[src$=\"#right\"],\n.json5e-class.markdown-preview-view span[src$=\"#left\"],\n.json5e-class.markdown-preview-view span[src$=\"#right\"],\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-class.markdown-preview-view img[src$=\"#left\"],\n.json5e-class.markdown-preview-view img[src$=\"#right\"],\n.json5e-class.markdown-source-view div[src$=\"#left\"],\n.json5e-class.markdown-source-view div[src$=\"#right\"],\n.json5e-class.markdown-source-view span[src$=\"#left\"],\n.json5e-class.markdown-source-view span[src$=\"#right\"],\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-class.markdown-source-view img[src$=\"#left\"],\n.json5e-class.markdown-source-view img[src$=\"#right\"],\n.json5e-deck.markdown-preview-view div[src$=\"#left\"],\n.json5e-deck.markdown-preview-view div[src$=\"#right\"],\n.json5e-deck.markdown-preview-view span[src$=\"#left\"],\n.json5e-deck.markdown-preview-view span[src$=\"#right\"],\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-deck.markdown-preview-view img[src$=\"#left\"],\n.json5e-deck.markdown-preview-view img[src$=\"#right\"],\n.json5e-deck.markdown-source-view div[src$=\"#left\"],\n.json5e-deck.markdown-source-view div[src$=\"#right\"],\n.json5e-deck.markdown-source-view span[src$=\"#left\"],\n.json5e-deck.markdown-source-view span[src$=\"#right\"],\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-deck.markdown-source-view img[src$=\"#left\"],\n.json5e-deck.markdown-source-view img[src$=\"#right\"],\n.json5e-deity.markdown-preview-view div[src$=\"#left\"],\n.json5e-deity.markdown-preview-view div[src$=\"#right\"],\n.json5e-deity.markdown-preview-view span[src$=\"#left\"],\n.json5e-deity.markdown-preview-view span[src$=\"#right\"],\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-deity.markdown-preview-view img[src$=\"#left\"],\n.json5e-deity.markdown-preview-view img[src$=\"#right\"],\n.json5e-deity.markdown-source-view div[src$=\"#left\"],\n.json5e-deity.markdown-source-view div[src$=\"#right\"],\n.json5e-deity.markdown-source-view span[src$=\"#left\"],\n.json5e-deity.markdown-source-view span[src$=\"#right\"],\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-deity.markdown-source-view img[src$=\"#left\"],\n.json5e-deity.markdown-source-view img[src$=\"#right\"],\n.json5e-feat.markdown-preview-view div[src$=\"#left\"],\n.json5e-feat.markdown-preview-view div[src$=\"#right\"],\n.json5e-feat.markdown-preview-view span[src$=\"#left\"],\n.json5e-feat.markdown-preview-view span[src$=\"#right\"],\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-feat.markdown-preview-view img[src$=\"#left\"],\n.json5e-feat.markdown-preview-view img[src$=\"#right\"],\n.json5e-feat.markdown-source-view div[src$=\"#left\"],\n.json5e-feat.markdown-source-view div[src$=\"#right\"],\n.json5e-feat.markdown-source-view span[src$=\"#left\"],\n.json5e-feat.markdown-source-view span[src$=\"#right\"],\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-feat.markdown-source-view img[src$=\"#left\"],\n.json5e-feat.markdown-source-view img[src$=\"#right\"],\n.json5e-hazard.markdown-preview-view div[src$=\"#left\"],\n.json5e-hazard.markdown-preview-view div[src$=\"#right\"],\n.json5e-hazard.markdown-preview-view span[src$=\"#left\"],\n.json5e-hazard.markdown-preview-view span[src$=\"#right\"],\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-hazard.markdown-preview-view img[src$=\"#left\"],\n.json5e-hazard.markdown-preview-view img[src$=\"#right\"],\n.json5e-hazard.markdown-source-view div[src$=\"#left\"],\n.json5e-hazard.markdown-source-view div[src$=\"#right\"],\n.json5e-hazard.markdown-source-view span[src$=\"#left\"],\n.json5e-hazard.markdown-source-view span[src$=\"#right\"],\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-hazard.markdown-source-view img[src$=\"#left\"],\n.json5e-hazard.markdown-source-view img[src$=\"#right\"],\n.json5e-item.markdown-preview-view div[src$=\"#left\"],\n.json5e-item.markdown-preview-view div[src$=\"#right\"],\n.json5e-item.markdown-preview-view span[src$=\"#left\"],\n.json5e-item.markdown-preview-view span[src$=\"#right\"],\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-item.markdown-preview-view img[src$=\"#left\"],\n.json5e-item.markdown-preview-view img[src$=\"#right\"],\n.json5e-item.markdown-source-view div[src$=\"#left\"],\n.json5e-item.markdown-source-view div[src$=\"#right\"],\n.json5e-item.markdown-source-view span[src$=\"#left\"],\n.json5e-item.markdown-source-view span[src$=\"#right\"],\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-item.markdown-source-view img[src$=\"#left\"],\n.json5e-item.markdown-source-view img[src$=\"#right\"],\n.json5e-monster.markdown-preview-view div[src$=\"#left\"],\n.json5e-monster.markdown-preview-view div[src$=\"#right\"],\n.json5e-monster.markdown-preview-view span[src$=\"#left\"],\n.json5e-monster.markdown-preview-view span[src$=\"#right\"],\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-monster.markdown-preview-view img[src$=\"#left\"],\n.json5e-monster.markdown-preview-view img[src$=\"#right\"],\n.json5e-monster.markdown-source-view div[src$=\"#left\"],\n.json5e-monster.markdown-source-view div[src$=\"#right\"],\n.json5e-monster.markdown-source-view span[src$=\"#left\"],\n.json5e-monster.markdown-source-view span[src$=\"#right\"],\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-monster.markdown-source-view img[src$=\"#left\"],\n.json5e-monster.markdown-source-view img[src$=\"#right\"],\n.json5e-note.markdown-preview-view div[src$=\"#left\"],\n.json5e-note.markdown-preview-view div[src$=\"#right\"],\n.json5e-note.markdown-preview-view span[src$=\"#left\"],\n.json5e-note.markdown-preview-view span[src$=\"#right\"],\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-note.markdown-preview-view img[src$=\"#left\"],\n.json5e-note.markdown-preview-view img[src$=\"#right\"],\n.json5e-note.markdown-source-view div[src$=\"#left\"],\n.json5e-note.markdown-source-view div[src$=\"#right\"],\n.json5e-note.markdown-source-view span[src$=\"#left\"],\n.json5e-note.markdown-source-view span[src$=\"#right\"],\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-note.markdown-source-view img[src$=\"#left\"],\n.json5e-note.markdown-source-view img[src$=\"#right\"],\n.json5e-object.markdown-preview-view div[src$=\"#left\"],\n.json5e-object.markdown-preview-view div[src$=\"#right\"],\n.json5e-object.markdown-preview-view span[src$=\"#left\"],\n.json5e-object.markdown-preview-view span[src$=\"#right\"],\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-object.markdown-preview-view img[src$=\"#left\"],\n.json5e-object.markdown-preview-view img[src$=\"#right\"],\n.json5e-object.markdown-source-view div[src$=\"#left\"],\n.json5e-object.markdown-source-view div[src$=\"#right\"],\n.json5e-object.markdown-source-view span[src$=\"#left\"],\n.json5e-object.markdown-source-view span[src$=\"#right\"],\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-object.markdown-source-view img[src$=\"#left\"],\n.json5e-object.markdown-source-view img[src$=\"#right\"],\n.json5e-psionic.markdown-preview-view div[src$=\"#left\"],\n.json5e-psionic.markdown-preview-view div[src$=\"#right\"],\n.json5e-psionic.markdown-preview-view span[src$=\"#left\"],\n.json5e-psionic.markdown-preview-view span[src$=\"#right\"],\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-psionic.markdown-preview-view img[src$=\"#left\"],\n.json5e-psionic.markdown-preview-view img[src$=\"#right\"],\n.json5e-psionic.markdown-source-view div[src$=\"#left\"],\n.json5e-psionic.markdown-source-view div[src$=\"#right\"],\n.json5e-psionic.markdown-source-view span[src$=\"#left\"],\n.json5e-psionic.markdown-source-view span[src$=\"#right\"],\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-psionic.markdown-source-view img[src$=\"#left\"],\n.json5e-psionic.markdown-source-view img[src$=\"#right\"],\n.json5e-race.markdown-preview-view div[src$=\"#left\"],\n.json5e-race.markdown-preview-view div[src$=\"#right\"],\n.json5e-race.markdown-preview-view span[src$=\"#left\"],\n.json5e-race.markdown-preview-view span[src$=\"#right\"],\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-race.markdown-preview-view img[src$=\"#left\"],\n.json5e-race.markdown-preview-view img[src$=\"#right\"],\n.json5e-race.markdown-source-view div[src$=\"#left\"],\n.json5e-race.markdown-source-view div[src$=\"#right\"],\n.json5e-race.markdown-source-view span[src$=\"#left\"],\n.json5e-race.markdown-source-view span[src$=\"#right\"],\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-race.markdown-source-view img[src$=\"#left\"],\n.json5e-race.markdown-source-view img[src$=\"#right\"],\n.json5e-reward.markdown-preview-view div[src$=\"#left\"],\n.json5e-reward.markdown-preview-view div[src$=\"#right\"],\n.json5e-reward.markdown-preview-view span[src$=\"#left\"],\n.json5e-reward.markdown-preview-view span[src$=\"#right\"],\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-reward.markdown-preview-view img[src$=\"#left\"],\n.json5e-reward.markdown-preview-view img[src$=\"#right\"],\n.json5e-reward.markdown-source-view div[src$=\"#left\"],\n.json5e-reward.markdown-source-view div[src$=\"#right\"],\n.json5e-reward.markdown-source-view span[src$=\"#left\"],\n.json5e-reward.markdown-source-view span[src$=\"#right\"],\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-reward.markdown-source-view img[src$=\"#left\"],\n.json5e-reward.markdown-source-view img[src$=\"#right\"],\n.json5e-species.markdown-preview-view div[src$=\"#left\"],\n.json5e-species.markdown-preview-view div[src$=\"#right\"],\n.json5e-species.markdown-preview-view span[src$=\"#left\"],\n.json5e-species.markdown-preview-view span[src$=\"#right\"],\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-species.markdown-preview-view img[src$=\"#left\"],\n.json5e-species.markdown-preview-view img[src$=\"#right\"],\n.json5e-species.markdown-source-view div[src$=\"#left\"],\n.json5e-species.markdown-source-view div[src$=\"#right\"],\n.json5e-species.markdown-source-view span[src$=\"#left\"],\n.json5e-species.markdown-source-view span[src$=\"#right\"],\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-species.markdown-source-view img[src$=\"#left\"],\n.json5e-species.markdown-source-view img[src$=\"#right\"],\n.json5e-spell.markdown-preview-view div[src$=\"#left\"],\n.json5e-spell.markdown-preview-view div[src$=\"#right\"],\n.json5e-spell.markdown-preview-view span[src$=\"#left\"],\n.json5e-spell.markdown-preview-view span[src$=\"#right\"],\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-spell.markdown-preview-view img[src$=\"#left\"],\n.json5e-spell.markdown-preview-view img[src$=\"#right\"],\n.json5e-spell.markdown-source-view div[src$=\"#left\"],\n.json5e-spell.markdown-source-view div[src$=\"#right\"],\n.json5e-spell.markdown-source-view span[src$=\"#left\"],\n.json5e-spell.markdown-source-view span[src$=\"#right\"],\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-spell.markdown-source-view img[src$=\"#left\"],\n.json5e-spell.markdown-source-view img[src$=\"#right\"],\n.json5e-vehicle.markdown-preview-view div[src$=\"#left\"],\n.json5e-vehicle.markdown-preview-view div[src$=\"#right\"],\n.json5e-vehicle.markdown-preview-view span[src$=\"#left\"],\n.json5e-vehicle.markdown-preview-view span[src$=\"#right\"],\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-vehicle.markdown-preview-view img[src$=\"#left\"],\n.json5e-vehicle.markdown-preview-view img[src$=\"#right\"],\n.json5e-vehicle.markdown-source-view div[src$=\"#left\"],\n.json5e-vehicle.markdown-source-view div[src$=\"#right\"],\n.json5e-vehicle.markdown-source-view span[src$=\"#left\"],\n.json5e-vehicle.markdown-source-view span[src$=\"#right\"],\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-vehicle.markdown-source-view img[src$=\"#left\"],\n.json5e-vehicle.markdown-source-view img[src$=\"#right\"] {\n  max-width: 50%;\n}\n.json5e-background.markdown-preview-view div[src$=\"#card\"], .json5e-background.markdown-preview-view div[src$=\"#token\"],\n.json5e-background.markdown-preview-view span[src$=\"#card\"],\n.json5e-background.markdown-preview-view span[src$=\"#token\"],\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-background.markdown-preview-view img[src$=\"#card\"],\n.json5e-background.markdown-preview-view img[src$=\"#token\"], .json5e-background.markdown-source-view div[src$=\"#card\"], .json5e-background.markdown-source-view div[src$=\"#token\"],\n.json5e-background.markdown-source-view span[src$=\"#card\"],\n.json5e-background.markdown-source-view span[src$=\"#token\"],\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-background.markdown-source-view img[src$=\"#card\"],\n.json5e-background.markdown-source-view img[src$=\"#token\"],\n.json5e-class.markdown-preview-view div[src$=\"#card\"],\n.json5e-class.markdown-preview-view div[src$=\"#token\"],\n.json5e-class.markdown-preview-view span[src$=\"#card\"],\n.json5e-class.markdown-preview-view span[src$=\"#token\"],\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-class.markdown-preview-view img[src$=\"#card\"],\n.json5e-class.markdown-preview-view img[src$=\"#token\"],\n.json5e-class.markdown-source-view div[src$=\"#card\"],\n.json5e-class.markdown-source-view div[src$=\"#token\"],\n.json5e-class.markdown-source-view span[src$=\"#card\"],\n.json5e-class.markdown-source-view span[src$=\"#token\"],\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-class.markdown-source-view img[src$=\"#card\"],\n.json5e-class.markdown-source-view img[src$=\"#token\"],\n.json5e-deck.markdown-preview-view div[src$=\"#card\"],\n.json5e-deck.markdown-preview-view div[src$=\"#token\"],\n.json5e-deck.markdown-preview-view span[src$=\"#card\"],\n.json5e-deck.markdown-preview-view span[src$=\"#token\"],\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-deck.markdown-preview-view img[src$=\"#card\"],\n.json5e-deck.markdown-preview-view img[src$=\"#token\"],\n.json5e-deck.markdown-source-view div[src$=\"#card\"],\n.json5e-deck.markdown-source-view div[src$=\"#token\"],\n.json5e-deck.markdown-source-view span[src$=\"#card\"],\n.json5e-deck.markdown-source-view span[src$=\"#token\"],\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-deck.markdown-source-view img[src$=\"#card\"],\n.json5e-deck.markdown-source-view img[src$=\"#token\"],\n.json5e-deity.markdown-preview-view div[src$=\"#card\"],\n.json5e-deity.markdown-preview-view div[src$=\"#token\"],\n.json5e-deity.markdown-preview-view span[src$=\"#card\"],\n.json5e-deity.markdown-preview-view span[src$=\"#token\"],\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-deity.markdown-preview-view img[src$=\"#card\"],\n.json5e-deity.markdown-preview-view img[src$=\"#token\"],\n.json5e-deity.markdown-source-view div[src$=\"#card\"],\n.json5e-deity.markdown-source-view div[src$=\"#token\"],\n.json5e-deity.markdown-source-view span[src$=\"#card\"],\n.json5e-deity.markdown-source-view span[src$=\"#token\"],\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-deity.markdown-source-view img[src$=\"#card\"],\n.json5e-deity.markdown-source-view img[src$=\"#token\"],\n.json5e-feat.markdown-preview-view div[src$=\"#card\"],\n.json5e-feat.markdown-preview-view div[src$=\"#token\"],\n.json5e-feat.markdown-preview-view span[src$=\"#card\"],\n.json5e-feat.markdown-preview-view span[src$=\"#token\"],\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-feat.markdown-preview-view img[src$=\"#card\"],\n.json5e-feat.markdown-preview-view img[src$=\"#token\"],\n.json5e-feat.markdown-source-view div[src$=\"#card\"],\n.json5e-feat.markdown-source-view div[src$=\"#token\"],\n.json5e-feat.markdown-source-view span[src$=\"#card\"],\n.json5e-feat.markdown-source-view span[src$=\"#token\"],\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-feat.markdown-source-view img[src$=\"#card\"],\n.json5e-feat.markdown-source-view img[src$=\"#token\"],\n.json5e-hazard.markdown-preview-view div[src$=\"#card\"],\n.json5e-hazard.markdown-preview-view div[src$=\"#token\"],\n.json5e-hazard.markdown-preview-view span[src$=\"#card\"],\n.json5e-hazard.markdown-preview-view span[src$=\"#token\"],\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-hazard.markdown-preview-view img[src$=\"#card\"],\n.json5e-hazard.markdown-preview-view img[src$=\"#token\"],\n.json5e-hazard.markdown-source-view div[src$=\"#card\"],\n.json5e-hazard.markdown-source-view div[src$=\"#token\"],\n.json5e-hazard.markdown-source-view span[src$=\"#card\"],\n.json5e-hazard.markdown-source-view span[src$=\"#token\"],\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-hazard.markdown-source-view img[src$=\"#card\"],\n.json5e-hazard.markdown-source-view img[src$=\"#token\"],\n.json5e-item.markdown-preview-view div[src$=\"#card\"],\n.json5e-item.markdown-preview-view div[src$=\"#token\"],\n.json5e-item.markdown-preview-view span[src$=\"#card\"],\n.json5e-item.markdown-preview-view span[src$=\"#token\"],\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-item.markdown-preview-view img[src$=\"#card\"],\n.json5e-item.markdown-preview-view img[src$=\"#token\"],\n.json5e-item.markdown-source-view div[src$=\"#card\"],\n.json5e-item.markdown-source-view div[src$=\"#token\"],\n.json5e-item.markdown-source-view span[src$=\"#card\"],\n.json5e-item.markdown-source-view span[src$=\"#token\"],\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-item.markdown-source-view img[src$=\"#card\"],\n.json5e-item.markdown-source-view img[src$=\"#token\"],\n.json5e-monster.markdown-preview-view div[src$=\"#card\"],\n.json5e-monster.markdown-preview-view div[src$=\"#token\"],\n.json5e-monster.markdown-preview-view span[src$=\"#card\"],\n.json5e-monster.markdown-preview-view span[src$=\"#token\"],\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-monster.markdown-preview-view img[src$=\"#card\"],\n.json5e-monster.markdown-preview-view img[src$=\"#token\"],\n.json5e-monster.markdown-source-view div[src$=\"#card\"],\n.json5e-monster.markdown-source-view div[src$=\"#token\"],\n.json5e-monster.markdown-source-view span[src$=\"#card\"],\n.json5e-monster.markdown-source-view span[src$=\"#token\"],\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-monster.markdown-source-view img[src$=\"#card\"],\n.json5e-monster.markdown-source-view img[src$=\"#token\"],\n.json5e-note.markdown-preview-view div[src$=\"#card\"],\n.json5e-note.markdown-preview-view div[src$=\"#token\"],\n.json5e-note.markdown-preview-view span[src$=\"#card\"],\n.json5e-note.markdown-preview-view span[src$=\"#token\"],\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-note.markdown-preview-view img[src$=\"#card\"],\n.json5e-note.markdown-preview-view img[src$=\"#token\"],\n.json5e-note.markdown-source-view div[src$=\"#card\"],\n.json5e-note.markdown-source-view div[src$=\"#token\"],\n.json5e-note.markdown-source-view span[src$=\"#card\"],\n.json5e-note.markdown-source-view span[src$=\"#token\"],\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-note.markdown-source-view img[src$=\"#card\"],\n.json5e-note.markdown-source-view img[src$=\"#token\"],\n.json5e-object.markdown-preview-view div[src$=\"#card\"],\n.json5e-object.markdown-preview-view div[src$=\"#token\"],\n.json5e-object.markdown-preview-view span[src$=\"#card\"],\n.json5e-object.markdown-preview-view span[src$=\"#token\"],\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-object.markdown-preview-view img[src$=\"#card\"],\n.json5e-object.markdown-preview-view img[src$=\"#token\"],\n.json5e-object.markdown-source-view div[src$=\"#card\"],\n.json5e-object.markdown-source-view div[src$=\"#token\"],\n.json5e-object.markdown-source-view span[src$=\"#card\"],\n.json5e-object.markdown-source-view span[src$=\"#token\"],\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-object.markdown-source-view img[src$=\"#card\"],\n.json5e-object.markdown-source-view img[src$=\"#token\"],\n.json5e-psionic.markdown-preview-view div[src$=\"#card\"],\n.json5e-psionic.markdown-preview-view div[src$=\"#token\"],\n.json5e-psionic.markdown-preview-view span[src$=\"#card\"],\n.json5e-psionic.markdown-preview-view span[src$=\"#token\"],\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-psionic.markdown-preview-view img[src$=\"#card\"],\n.json5e-psionic.markdown-preview-view img[src$=\"#token\"],\n.json5e-psionic.markdown-source-view div[src$=\"#card\"],\n.json5e-psionic.markdown-source-view div[src$=\"#token\"],\n.json5e-psionic.markdown-source-view span[src$=\"#card\"],\n.json5e-psionic.markdown-source-view span[src$=\"#token\"],\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-psionic.markdown-source-view img[src$=\"#card\"],\n.json5e-psionic.markdown-source-view img[src$=\"#token\"],\n.json5e-race.markdown-preview-view div[src$=\"#card\"],\n.json5e-race.markdown-preview-view div[src$=\"#token\"],\n.json5e-race.markdown-preview-view span[src$=\"#card\"],\n.json5e-race.markdown-preview-view span[src$=\"#token\"],\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-race.markdown-preview-view img[src$=\"#card\"],\n.json5e-race.markdown-preview-view img[src$=\"#token\"],\n.json5e-race.markdown-source-view div[src$=\"#card\"],\n.json5e-race.markdown-source-view div[src$=\"#token\"],\n.json5e-race.markdown-source-view span[src$=\"#card\"],\n.json5e-race.markdown-source-view span[src$=\"#token\"],\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-race.markdown-source-view img[src$=\"#card\"],\n.json5e-race.markdown-source-view img[src$=\"#token\"],\n.json5e-reward.markdown-preview-view div[src$=\"#card\"],\n.json5e-reward.markdown-preview-view div[src$=\"#token\"],\n.json5e-reward.markdown-preview-view span[src$=\"#card\"],\n.json5e-reward.markdown-preview-view span[src$=\"#token\"],\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-reward.markdown-preview-view img[src$=\"#card\"],\n.json5e-reward.markdown-preview-view img[src$=\"#token\"],\n.json5e-reward.markdown-source-view div[src$=\"#card\"],\n.json5e-reward.markdown-source-view div[src$=\"#token\"],\n.json5e-reward.markdown-source-view span[src$=\"#card\"],\n.json5e-reward.markdown-source-view span[src$=\"#token\"],\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-reward.markdown-source-view img[src$=\"#card\"],\n.json5e-reward.markdown-source-view img[src$=\"#token\"],\n.json5e-species.markdown-preview-view div[src$=\"#card\"],\n.json5e-species.markdown-preview-view div[src$=\"#token\"],\n.json5e-species.markdown-preview-view span[src$=\"#card\"],\n.json5e-species.markdown-preview-view span[src$=\"#token\"],\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-species.markdown-preview-view img[src$=\"#card\"],\n.json5e-species.markdown-preview-view img[src$=\"#token\"],\n.json5e-species.markdown-source-view div[src$=\"#card\"],\n.json5e-species.markdown-source-view div[src$=\"#token\"],\n.json5e-species.markdown-source-view span[src$=\"#card\"],\n.json5e-species.markdown-source-view span[src$=\"#token\"],\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-species.markdown-source-view img[src$=\"#card\"],\n.json5e-species.markdown-source-view img[src$=\"#token\"],\n.json5e-spell.markdown-preview-view div[src$=\"#card\"],\n.json5e-spell.markdown-preview-view div[src$=\"#token\"],\n.json5e-spell.markdown-preview-view span[src$=\"#card\"],\n.json5e-spell.markdown-preview-view span[src$=\"#token\"],\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-spell.markdown-preview-view img[src$=\"#card\"],\n.json5e-spell.markdown-preview-view img[src$=\"#token\"],\n.json5e-spell.markdown-source-view div[src$=\"#card\"],\n.json5e-spell.markdown-source-view div[src$=\"#token\"],\n.json5e-spell.markdown-source-view span[src$=\"#card\"],\n.json5e-spell.markdown-source-view span[src$=\"#token\"],\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-spell.markdown-source-view img[src$=\"#card\"],\n.json5e-spell.markdown-source-view img[src$=\"#token\"],\n.json5e-vehicle.markdown-preview-view div[src$=\"#card\"],\n.json5e-vehicle.markdown-preview-view div[src$=\"#token\"],\n.json5e-vehicle.markdown-preview-view span[src$=\"#card\"],\n.json5e-vehicle.markdown-preview-view span[src$=\"#token\"],\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-vehicle.markdown-preview-view img[src$=\"#card\"],\n.json5e-vehicle.markdown-preview-view img[src$=\"#token\"],\n.json5e-vehicle.markdown-source-view div[src$=\"#card\"],\n.json5e-vehicle.markdown-source-view div[src$=\"#token\"],\n.json5e-vehicle.markdown-source-view span[src$=\"#card\"],\n.json5e-vehicle.markdown-source-view span[src$=\"#token\"],\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-vehicle.markdown-source-view img[src$=\"#card\"],\n.json5e-vehicle.markdown-source-view img[src$=\"#token\"] {\n  width: 150px;\n}\n.json5e-background.markdown-preview-view div[src$=\"#symbol\"], .json5e-background.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-background.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-background.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-background.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-background.markdown-preview-view img[src$=\"#portrait\"], .json5e-background.markdown-source-view div[src$=\"#symbol\"], .json5e-background.markdown-source-view div[src$=\"#portrait\"],\n.json5e-background.markdown-source-view span[src$=\"#symbol\"],\n.json5e-background.markdown-source-view span[src$=\"#portrait\"],\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-background.markdown-source-view img[src$=\"#symbol\"],\n.json5e-background.markdown-source-view img[src$=\"#portrait\"],\n.json5e-class.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-class.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-class.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-class.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-class.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-class.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-class.markdown-source-view div[src$=\"#symbol\"],\n.json5e-class.markdown-source-view div[src$=\"#portrait\"],\n.json5e-class.markdown-source-view span[src$=\"#symbol\"],\n.json5e-class.markdown-source-view span[src$=\"#portrait\"],\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-class.markdown-source-view img[src$=\"#symbol\"],\n.json5e-class.markdown-source-view img[src$=\"#portrait\"],\n.json5e-deck.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-deck.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-deck.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-deck.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-deck.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-deck.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-deck.markdown-source-view div[src$=\"#symbol\"],\n.json5e-deck.markdown-source-view div[src$=\"#portrait\"],\n.json5e-deck.markdown-source-view span[src$=\"#symbol\"],\n.json5e-deck.markdown-source-view span[src$=\"#portrait\"],\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-deck.markdown-source-view img[src$=\"#symbol\"],\n.json5e-deck.markdown-source-view img[src$=\"#portrait\"],\n.json5e-deity.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-deity.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-deity.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-deity.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-deity.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-deity.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-deity.markdown-source-view div[src$=\"#symbol\"],\n.json5e-deity.markdown-source-view div[src$=\"#portrait\"],\n.json5e-deity.markdown-source-view span[src$=\"#symbol\"],\n.json5e-deity.markdown-source-view span[src$=\"#portrait\"],\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-deity.markdown-source-view img[src$=\"#symbol\"],\n.json5e-deity.markdown-source-view img[src$=\"#portrait\"],\n.json5e-feat.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-feat.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-feat.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-feat.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-feat.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-feat.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-feat.markdown-source-view div[src$=\"#symbol\"],\n.json5e-feat.markdown-source-view div[src$=\"#portrait\"],\n.json5e-feat.markdown-source-view span[src$=\"#symbol\"],\n.json5e-feat.markdown-source-view span[src$=\"#portrait\"],\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-feat.markdown-source-view img[src$=\"#symbol\"],\n.json5e-feat.markdown-source-view img[src$=\"#portrait\"],\n.json5e-hazard.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-hazard.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-hazard.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-hazard.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-hazard.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-hazard.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-hazard.markdown-source-view div[src$=\"#symbol\"],\n.json5e-hazard.markdown-source-view div[src$=\"#portrait\"],\n.json5e-hazard.markdown-source-view span[src$=\"#symbol\"],\n.json5e-hazard.markdown-source-view span[src$=\"#portrait\"],\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-hazard.markdown-source-view img[src$=\"#symbol\"],\n.json5e-hazard.markdown-source-view img[src$=\"#portrait\"],\n.json5e-item.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-item.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-item.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-item.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-item.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-item.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-item.markdown-source-view div[src$=\"#symbol\"],\n.json5e-item.markdown-source-view div[src$=\"#portrait\"],\n.json5e-item.markdown-source-view span[src$=\"#symbol\"],\n.json5e-item.markdown-source-view span[src$=\"#portrait\"],\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-item.markdown-source-view img[src$=\"#symbol\"],\n.json5e-item.markdown-source-view img[src$=\"#portrait\"],\n.json5e-monster.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-monster.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-monster.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-monster.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-monster.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-monster.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-monster.markdown-source-view div[src$=\"#symbol\"],\n.json5e-monster.markdown-source-view div[src$=\"#portrait\"],\n.json5e-monster.markdown-source-view span[src$=\"#symbol\"],\n.json5e-monster.markdown-source-view span[src$=\"#portrait\"],\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-monster.markdown-source-view img[src$=\"#symbol\"],\n.json5e-monster.markdown-source-view img[src$=\"#portrait\"],\n.json5e-note.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-note.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-note.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-note.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-note.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-note.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-note.markdown-source-view div[src$=\"#symbol\"],\n.json5e-note.markdown-source-view div[src$=\"#portrait\"],\n.json5e-note.markdown-source-view span[src$=\"#symbol\"],\n.json5e-note.markdown-source-view span[src$=\"#portrait\"],\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-note.markdown-source-view img[src$=\"#symbol\"],\n.json5e-note.markdown-source-view img[src$=\"#portrait\"],\n.json5e-object.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-object.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-object.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-object.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-object.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-object.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-object.markdown-source-view div[src$=\"#symbol\"],\n.json5e-object.markdown-source-view div[src$=\"#portrait\"],\n.json5e-object.markdown-source-view span[src$=\"#symbol\"],\n.json5e-object.markdown-source-view span[src$=\"#portrait\"],\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-object.markdown-source-view img[src$=\"#symbol\"],\n.json5e-object.markdown-source-view img[src$=\"#portrait\"],\n.json5e-psionic.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-psionic.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-psionic.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-psionic.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-psionic.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-psionic.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-psionic.markdown-source-view div[src$=\"#symbol\"],\n.json5e-psionic.markdown-source-view div[src$=\"#portrait\"],\n.json5e-psionic.markdown-source-view span[src$=\"#symbol\"],\n.json5e-psionic.markdown-source-view span[src$=\"#portrait\"],\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-psionic.markdown-source-view img[src$=\"#symbol\"],\n.json5e-psionic.markdown-source-view img[src$=\"#portrait\"],\n.json5e-race.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-race.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-race.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-race.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-race.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-race.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-race.markdown-source-view div[src$=\"#symbol\"],\n.json5e-race.markdown-source-view div[src$=\"#portrait\"],\n.json5e-race.markdown-source-view span[src$=\"#symbol\"],\n.json5e-race.markdown-source-view span[src$=\"#portrait\"],\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-race.markdown-source-view img[src$=\"#symbol\"],\n.json5e-race.markdown-source-view img[src$=\"#portrait\"],\n.json5e-reward.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-reward.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-reward.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-reward.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-reward.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-reward.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-reward.markdown-source-view div[src$=\"#symbol\"],\n.json5e-reward.markdown-source-view div[src$=\"#portrait\"],\n.json5e-reward.markdown-source-view span[src$=\"#symbol\"],\n.json5e-reward.markdown-source-view span[src$=\"#portrait\"],\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-reward.markdown-source-view img[src$=\"#symbol\"],\n.json5e-reward.markdown-source-view img[src$=\"#portrait\"],\n.json5e-species.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-species.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-species.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-species.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-species.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-species.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-species.markdown-source-view div[src$=\"#symbol\"],\n.json5e-species.markdown-source-view div[src$=\"#portrait\"],\n.json5e-species.markdown-source-view span[src$=\"#symbol\"],\n.json5e-species.markdown-source-view span[src$=\"#portrait\"],\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-species.markdown-source-view img[src$=\"#symbol\"],\n.json5e-species.markdown-source-view img[src$=\"#portrait\"],\n.json5e-spell.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-spell.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-spell.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-spell.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-spell.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-spell.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-spell.markdown-source-view div[src$=\"#symbol\"],\n.json5e-spell.markdown-source-view div[src$=\"#portrait\"],\n.json5e-spell.markdown-source-view span[src$=\"#symbol\"],\n.json5e-spell.markdown-source-view span[src$=\"#portrait\"],\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-spell.markdown-source-view img[src$=\"#symbol\"],\n.json5e-spell.markdown-source-view img[src$=\"#portrait\"],\n.json5e-vehicle.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-vehicle.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-vehicle.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-vehicle.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-vehicle.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-vehicle.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-vehicle.markdown-source-view div[src$=\"#symbol\"],\n.json5e-vehicle.markdown-source-view div[src$=\"#portrait\"],\n.json5e-vehicle.markdown-source-view span[src$=\"#symbol\"],\n.json5e-vehicle.markdown-source-view span[src$=\"#portrait\"],\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-vehicle.markdown-source-view img[src$=\"#symbol\"],\n.json5e-vehicle.markdown-source-view img[src$=\"#portrait\"] {\n  width: 200px;\n}\n\n/* For decks of cards, ensure images don't overlap */\n.json5e-deck h3 {\n  clear: both;\n}\n\n.json5e-background .inline-title,\n.json5e-class .inline-title,\n.json5e-deck .inline-title,\n.json5e-deity .inline-title,\n.json5e-feat .inline-title,\n.json5e-hazard .inline-title,\n.json5e-item .inline-title,\n.json5e-monster .inline-title,\n.json5e-note .inline-title,\n.json5e-object .inline-title,\n.json5e-psionic .inline-title,\n.json5e-race .inline-title,\n.json5e-reward .inline-title,\n.json5e-spell .inline-title,\n.json5e-vehicle .inline-title {\n  display: none;\n}\n\n.creature-view-container.workspace-leaf-content {\n  background-color: var(--background-primary-alt);\n}\n.creature-view-container.workspace-leaf-content .admonition {\n  margin-top: 0;\n}\n\n@media (min-width: 600px) {\n  .json5e-index ul {\n    columns: 2;\n  }\n}\n"
  },
  {
    "path": "examples/css-snippets/dnd5e-float-images.css",
    "content": "/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/dnd5e-float-images.scss */\np div.image-embed:hover::after,\np span.image-embed:hover::after,\np img:hover::after,\ndiv div.image-embed:hover::after,\ndiv span.image-embed:hover::after,\ndiv img:hover::after,\nspan div.image-embed:hover::after,\nspan span.image-embed:hover::after,\nspan img:hover::after {\n  content: attr(alt);\n  position: absolute;\n  background: rgba(0, 0, 0, 0.8);\n  color: white;\n  padding: 4px 8px;\n  border-radius: 4px;\n  font-size: 0.8em;\n  width: 200px;\n  max-width: 200px;\n  z-index: 10;\n  top: 2em;\n  left: 50%;\n  transform: translateX(-50%);\n}\n\n.json5e-background.markdown-preview-view div.image-embed,\n.json5e-background.markdown-preview-view span.image-embed,\n.json5e-background.markdown-preview-view span.internal-embed.image-embed,\n.json5e-background.markdown-preview-view img.image-embed, .json5e-background.markdown-source-view div.image-embed,\n.json5e-background.markdown-source-view span.image-embed,\n.json5e-background.markdown-source-view span.internal-embed.image-embed,\n.json5e-background.markdown-source-view img.image-embed,\n.json5e-class.markdown-preview-view div.image-embed,\n.json5e-class.markdown-preview-view span.image-embed,\n.json5e-class.markdown-preview-view span.internal-embed.image-embed,\n.json5e-class.markdown-preview-view img.image-embed,\n.json5e-class.markdown-source-view div.image-embed,\n.json5e-class.markdown-source-view span.image-embed,\n.json5e-class.markdown-source-view span.internal-embed.image-embed,\n.json5e-class.markdown-source-view img.image-embed,\n.json5e-deck.markdown-preview-view div.image-embed,\n.json5e-deck.markdown-preview-view span.image-embed,\n.json5e-deck.markdown-preview-view span.internal-embed.image-embed,\n.json5e-deck.markdown-preview-view img.image-embed,\n.json5e-deck.markdown-source-view div.image-embed,\n.json5e-deck.markdown-source-view span.image-embed,\n.json5e-deck.markdown-source-view span.internal-embed.image-embed,\n.json5e-deck.markdown-source-view img.image-embed,\n.json5e-deity.markdown-preview-view div.image-embed,\n.json5e-deity.markdown-preview-view span.image-embed,\n.json5e-deity.markdown-preview-view span.internal-embed.image-embed,\n.json5e-deity.markdown-preview-view img.image-embed,\n.json5e-deity.markdown-source-view div.image-embed,\n.json5e-deity.markdown-source-view span.image-embed,\n.json5e-deity.markdown-source-view span.internal-embed.image-embed,\n.json5e-deity.markdown-source-view img.image-embed,\n.json5e-feat.markdown-preview-view div.image-embed,\n.json5e-feat.markdown-preview-view span.image-embed,\n.json5e-feat.markdown-preview-view span.internal-embed.image-embed,\n.json5e-feat.markdown-preview-view img.image-embed,\n.json5e-feat.markdown-source-view div.image-embed,\n.json5e-feat.markdown-source-view span.image-embed,\n.json5e-feat.markdown-source-view span.internal-embed.image-embed,\n.json5e-feat.markdown-source-view img.image-embed,\n.json5e-hazard.markdown-preview-view div.image-embed,\n.json5e-hazard.markdown-preview-view span.image-embed,\n.json5e-hazard.markdown-preview-view span.internal-embed.image-embed,\n.json5e-hazard.markdown-preview-view img.image-embed,\n.json5e-hazard.markdown-source-view div.image-embed,\n.json5e-hazard.markdown-source-view span.image-embed,\n.json5e-hazard.markdown-source-view span.internal-embed.image-embed,\n.json5e-hazard.markdown-source-view img.image-embed,\n.json5e-item.markdown-preview-view div.image-embed,\n.json5e-item.markdown-preview-view span.image-embed,\n.json5e-item.markdown-preview-view span.internal-embed.image-embed,\n.json5e-item.markdown-preview-view img.image-embed,\n.json5e-item.markdown-source-view div.image-embed,\n.json5e-item.markdown-source-view span.image-embed,\n.json5e-item.markdown-source-view span.internal-embed.image-embed,\n.json5e-item.markdown-source-view img.image-embed,\n.json5e-monster.markdown-preview-view div.image-embed,\n.json5e-monster.markdown-preview-view span.image-embed,\n.json5e-monster.markdown-preview-view span.internal-embed.image-embed,\n.json5e-monster.markdown-preview-view img.image-embed,\n.json5e-monster.markdown-source-view div.image-embed,\n.json5e-monster.markdown-source-view span.image-embed,\n.json5e-monster.markdown-source-view span.internal-embed.image-embed,\n.json5e-monster.markdown-source-view img.image-embed,\n.json5e-note.markdown-preview-view div.image-embed,\n.json5e-note.markdown-preview-view span.image-embed,\n.json5e-note.markdown-preview-view span.internal-embed.image-embed,\n.json5e-note.markdown-preview-view img.image-embed,\n.json5e-note.markdown-source-view div.image-embed,\n.json5e-note.markdown-source-view span.image-embed,\n.json5e-note.markdown-source-view span.internal-embed.image-embed,\n.json5e-note.markdown-source-view img.image-embed,\n.json5e-object.markdown-preview-view div.image-embed,\n.json5e-object.markdown-preview-view span.image-embed,\n.json5e-object.markdown-preview-view span.internal-embed.image-embed,\n.json5e-object.markdown-preview-view img.image-embed,\n.json5e-object.markdown-source-view div.image-embed,\n.json5e-object.markdown-source-view span.image-embed,\n.json5e-object.markdown-source-view span.internal-embed.image-embed,\n.json5e-object.markdown-source-view img.image-embed,\n.json5e-psionic.markdown-preview-view div.image-embed,\n.json5e-psionic.markdown-preview-view span.image-embed,\n.json5e-psionic.markdown-preview-view span.internal-embed.image-embed,\n.json5e-psionic.markdown-preview-view img.image-embed,\n.json5e-psionic.markdown-source-view div.image-embed,\n.json5e-psionic.markdown-source-view span.image-embed,\n.json5e-psionic.markdown-source-view span.internal-embed.image-embed,\n.json5e-psionic.markdown-source-view img.image-embed,\n.json5e-race.markdown-preview-view div.image-embed,\n.json5e-race.markdown-preview-view span.image-embed,\n.json5e-race.markdown-preview-view span.internal-embed.image-embed,\n.json5e-race.markdown-preview-view img.image-embed,\n.json5e-race.markdown-source-view div.image-embed,\n.json5e-race.markdown-source-view span.image-embed,\n.json5e-race.markdown-source-view span.internal-embed.image-embed,\n.json5e-race.markdown-source-view img.image-embed,\n.json5e-reward.markdown-preview-view div.image-embed,\n.json5e-reward.markdown-preview-view span.image-embed,\n.json5e-reward.markdown-preview-view span.internal-embed.image-embed,\n.json5e-reward.markdown-preview-view img.image-embed,\n.json5e-reward.markdown-source-view div.image-embed,\n.json5e-reward.markdown-source-view span.image-embed,\n.json5e-reward.markdown-source-view span.internal-embed.image-embed,\n.json5e-reward.markdown-source-view img.image-embed,\n.json5e-species.markdown-preview-view div.image-embed,\n.json5e-species.markdown-preview-view span.image-embed,\n.json5e-species.markdown-preview-view span.internal-embed.image-embed,\n.json5e-species.markdown-preview-view img.image-embed,\n.json5e-species.markdown-source-view div.image-embed,\n.json5e-species.markdown-source-view span.image-embed,\n.json5e-species.markdown-source-view span.internal-embed.image-embed,\n.json5e-species.markdown-source-view img.image-embed,\n.json5e-spell.markdown-preview-view div.image-embed,\n.json5e-spell.markdown-preview-view span.image-embed,\n.json5e-spell.markdown-preview-view span.internal-embed.image-embed,\n.json5e-spell.markdown-preview-view img.image-embed,\n.json5e-spell.markdown-source-view div.image-embed,\n.json5e-spell.markdown-source-view span.image-embed,\n.json5e-spell.markdown-source-view span.internal-embed.image-embed,\n.json5e-spell.markdown-source-view img.image-embed,\n.json5e-vehicle.markdown-preview-view div.image-embed,\n.json5e-vehicle.markdown-preview-view span.image-embed,\n.json5e-vehicle.markdown-preview-view span.internal-embed.image-embed,\n.json5e-vehicle.markdown-preview-view img.image-embed,\n.json5e-vehicle.markdown-source-view div.image-embed,\n.json5e-vehicle.markdown-source-view span.image-embed,\n.json5e-vehicle.markdown-source-view span.internal-embed.image-embed,\n.json5e-vehicle.markdown-source-view img.image-embed {\n  position: relative;\n  /* Display the alt text after the image */\n  /*\n  &::after {\n    content: attr(alt);\n    display: block;\n    margin-top: 0.5em;\n    top: 100%; // Positions it below the parent element\n    left: 0;\n  }\n  */\n}\n.json5e-background.markdown-preview-view div[src$=\"#center\"],\n.json5e-background.markdown-preview-view span[src$=\"#center\"],\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-background.markdown-preview-view img[src$=\"#center\"], .json5e-background.markdown-source-view div[src$=\"#center\"],\n.json5e-background.markdown-source-view span[src$=\"#center\"],\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-background.markdown-source-view img[src$=\"#center\"],\n.json5e-class.markdown-preview-view div[src$=\"#center\"],\n.json5e-class.markdown-preview-view span[src$=\"#center\"],\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-class.markdown-preview-view img[src$=\"#center\"],\n.json5e-class.markdown-source-view div[src$=\"#center\"],\n.json5e-class.markdown-source-view span[src$=\"#center\"],\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-class.markdown-source-view img[src$=\"#center\"],\n.json5e-deck.markdown-preview-view div[src$=\"#center\"],\n.json5e-deck.markdown-preview-view span[src$=\"#center\"],\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-deck.markdown-preview-view img[src$=\"#center\"],\n.json5e-deck.markdown-source-view div[src$=\"#center\"],\n.json5e-deck.markdown-source-view span[src$=\"#center\"],\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-deck.markdown-source-view img[src$=\"#center\"],\n.json5e-deity.markdown-preview-view div[src$=\"#center\"],\n.json5e-deity.markdown-preview-view span[src$=\"#center\"],\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-deity.markdown-preview-view img[src$=\"#center\"],\n.json5e-deity.markdown-source-view div[src$=\"#center\"],\n.json5e-deity.markdown-source-view span[src$=\"#center\"],\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-deity.markdown-source-view img[src$=\"#center\"],\n.json5e-feat.markdown-preview-view div[src$=\"#center\"],\n.json5e-feat.markdown-preview-view span[src$=\"#center\"],\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-feat.markdown-preview-view img[src$=\"#center\"],\n.json5e-feat.markdown-source-view div[src$=\"#center\"],\n.json5e-feat.markdown-source-view span[src$=\"#center\"],\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-feat.markdown-source-view img[src$=\"#center\"],\n.json5e-hazard.markdown-preview-view div[src$=\"#center\"],\n.json5e-hazard.markdown-preview-view span[src$=\"#center\"],\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-hazard.markdown-preview-view img[src$=\"#center\"],\n.json5e-hazard.markdown-source-view div[src$=\"#center\"],\n.json5e-hazard.markdown-source-view span[src$=\"#center\"],\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-hazard.markdown-source-view img[src$=\"#center\"],\n.json5e-item.markdown-preview-view div[src$=\"#center\"],\n.json5e-item.markdown-preview-view span[src$=\"#center\"],\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-item.markdown-preview-view img[src$=\"#center\"],\n.json5e-item.markdown-source-view div[src$=\"#center\"],\n.json5e-item.markdown-source-view span[src$=\"#center\"],\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-item.markdown-source-view img[src$=\"#center\"],\n.json5e-monster.markdown-preview-view div[src$=\"#center\"],\n.json5e-monster.markdown-preview-view span[src$=\"#center\"],\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-monster.markdown-preview-view img[src$=\"#center\"],\n.json5e-monster.markdown-source-view div[src$=\"#center\"],\n.json5e-monster.markdown-source-view span[src$=\"#center\"],\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-monster.markdown-source-view img[src$=\"#center\"],\n.json5e-note.markdown-preview-view div[src$=\"#center\"],\n.json5e-note.markdown-preview-view span[src$=\"#center\"],\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-note.markdown-preview-view img[src$=\"#center\"],\n.json5e-note.markdown-source-view div[src$=\"#center\"],\n.json5e-note.markdown-source-view span[src$=\"#center\"],\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-note.markdown-source-view img[src$=\"#center\"],\n.json5e-object.markdown-preview-view div[src$=\"#center\"],\n.json5e-object.markdown-preview-view span[src$=\"#center\"],\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-object.markdown-preview-view img[src$=\"#center\"],\n.json5e-object.markdown-source-view div[src$=\"#center\"],\n.json5e-object.markdown-source-view span[src$=\"#center\"],\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-object.markdown-source-view img[src$=\"#center\"],\n.json5e-psionic.markdown-preview-view div[src$=\"#center\"],\n.json5e-psionic.markdown-preview-view span[src$=\"#center\"],\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-psionic.markdown-preview-view img[src$=\"#center\"],\n.json5e-psionic.markdown-source-view div[src$=\"#center\"],\n.json5e-psionic.markdown-source-view span[src$=\"#center\"],\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-psionic.markdown-source-view img[src$=\"#center\"],\n.json5e-race.markdown-preview-view div[src$=\"#center\"],\n.json5e-race.markdown-preview-view span[src$=\"#center\"],\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-race.markdown-preview-view img[src$=\"#center\"],\n.json5e-race.markdown-source-view div[src$=\"#center\"],\n.json5e-race.markdown-source-view span[src$=\"#center\"],\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-race.markdown-source-view img[src$=\"#center\"],\n.json5e-reward.markdown-preview-view div[src$=\"#center\"],\n.json5e-reward.markdown-preview-view span[src$=\"#center\"],\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-reward.markdown-preview-view img[src$=\"#center\"],\n.json5e-reward.markdown-source-view div[src$=\"#center\"],\n.json5e-reward.markdown-source-view span[src$=\"#center\"],\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-reward.markdown-source-view img[src$=\"#center\"],\n.json5e-species.markdown-preview-view div[src$=\"#center\"],\n.json5e-species.markdown-preview-view span[src$=\"#center\"],\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-species.markdown-preview-view img[src$=\"#center\"],\n.json5e-species.markdown-source-view div[src$=\"#center\"],\n.json5e-species.markdown-source-view span[src$=\"#center\"],\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-species.markdown-source-view img[src$=\"#center\"],\n.json5e-spell.markdown-preview-view div[src$=\"#center\"],\n.json5e-spell.markdown-preview-view span[src$=\"#center\"],\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-spell.markdown-preview-view img[src$=\"#center\"],\n.json5e-spell.markdown-source-view div[src$=\"#center\"],\n.json5e-spell.markdown-source-view span[src$=\"#center\"],\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-spell.markdown-source-view img[src$=\"#center\"],\n.json5e-vehicle.markdown-preview-view div[src$=\"#center\"],\n.json5e-vehicle.markdown-preview-view span[src$=\"#center\"],\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#center\"],\n.json5e-vehicle.markdown-preview-view img[src$=\"#center\"],\n.json5e-vehicle.markdown-source-view div[src$=\"#center\"],\n.json5e-vehicle.markdown-source-view span[src$=\"#center\"],\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#center\"],\n.json5e-vehicle.markdown-source-view img[src$=\"#center\"] {\n  display: flex;\n  width: 100%;\n  justify-content: center;\n  align-items: center;\n}\n.json5e-background.markdown-preview-view div[src$=\"#card\"], .json5e-background.markdown-preview-view div[src$=\"#symbol\"], .json5e-background.markdown-preview-view div[src$=\"#portrait\"], .json5e-background.markdown-preview-view div[src$=\"#token\"], .json5e-background.markdown-preview-view div[src$=\"#right\"],\n.json5e-background.markdown-preview-view span[src$=\"#card\"],\n.json5e-background.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-background.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-background.markdown-preview-view span[src$=\"#token\"],\n.json5e-background.markdown-preview-view span[src$=\"#right\"],\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-background.markdown-preview-view img[src$=\"#card\"],\n.json5e-background.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-background.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-background.markdown-preview-view img[src$=\"#token\"],\n.json5e-background.markdown-preview-view img[src$=\"#right\"], .json5e-background.markdown-source-view div[src$=\"#card\"], .json5e-background.markdown-source-view div[src$=\"#symbol\"], .json5e-background.markdown-source-view div[src$=\"#portrait\"], .json5e-background.markdown-source-view div[src$=\"#token\"], .json5e-background.markdown-source-view div[src$=\"#right\"],\n.json5e-background.markdown-source-view span[src$=\"#card\"],\n.json5e-background.markdown-source-view span[src$=\"#symbol\"],\n.json5e-background.markdown-source-view span[src$=\"#portrait\"],\n.json5e-background.markdown-source-view span[src$=\"#token\"],\n.json5e-background.markdown-source-view span[src$=\"#right\"],\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-background.markdown-source-view img[src$=\"#card\"],\n.json5e-background.markdown-source-view img[src$=\"#symbol\"],\n.json5e-background.markdown-source-view img[src$=\"#portrait\"],\n.json5e-background.markdown-source-view img[src$=\"#token\"],\n.json5e-background.markdown-source-view img[src$=\"#right\"],\n.json5e-class.markdown-preview-view div[src$=\"#card\"],\n.json5e-class.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-class.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-class.markdown-preview-view div[src$=\"#token\"],\n.json5e-class.markdown-preview-view div[src$=\"#right\"],\n.json5e-class.markdown-preview-view span[src$=\"#card\"],\n.json5e-class.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-class.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-class.markdown-preview-view span[src$=\"#token\"],\n.json5e-class.markdown-preview-view span[src$=\"#right\"],\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-class.markdown-preview-view img[src$=\"#card\"],\n.json5e-class.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-class.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-class.markdown-preview-view img[src$=\"#token\"],\n.json5e-class.markdown-preview-view img[src$=\"#right\"],\n.json5e-class.markdown-source-view div[src$=\"#card\"],\n.json5e-class.markdown-source-view div[src$=\"#symbol\"],\n.json5e-class.markdown-source-view div[src$=\"#portrait\"],\n.json5e-class.markdown-source-view div[src$=\"#token\"],\n.json5e-class.markdown-source-view div[src$=\"#right\"],\n.json5e-class.markdown-source-view span[src$=\"#card\"],\n.json5e-class.markdown-source-view span[src$=\"#symbol\"],\n.json5e-class.markdown-source-view span[src$=\"#portrait\"],\n.json5e-class.markdown-source-view span[src$=\"#token\"],\n.json5e-class.markdown-source-view span[src$=\"#right\"],\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-class.markdown-source-view img[src$=\"#card\"],\n.json5e-class.markdown-source-view img[src$=\"#symbol\"],\n.json5e-class.markdown-source-view img[src$=\"#portrait\"],\n.json5e-class.markdown-source-view img[src$=\"#token\"],\n.json5e-class.markdown-source-view img[src$=\"#right\"],\n.json5e-deck.markdown-preview-view div[src$=\"#card\"],\n.json5e-deck.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-deck.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-deck.markdown-preview-view div[src$=\"#token\"],\n.json5e-deck.markdown-preview-view div[src$=\"#right\"],\n.json5e-deck.markdown-preview-view span[src$=\"#card\"],\n.json5e-deck.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-deck.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-deck.markdown-preview-view span[src$=\"#token\"],\n.json5e-deck.markdown-preview-view span[src$=\"#right\"],\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-deck.markdown-preview-view img[src$=\"#card\"],\n.json5e-deck.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-deck.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-deck.markdown-preview-view img[src$=\"#token\"],\n.json5e-deck.markdown-preview-view img[src$=\"#right\"],\n.json5e-deck.markdown-source-view div[src$=\"#card\"],\n.json5e-deck.markdown-source-view div[src$=\"#symbol\"],\n.json5e-deck.markdown-source-view div[src$=\"#portrait\"],\n.json5e-deck.markdown-source-view div[src$=\"#token\"],\n.json5e-deck.markdown-source-view div[src$=\"#right\"],\n.json5e-deck.markdown-source-view span[src$=\"#card\"],\n.json5e-deck.markdown-source-view span[src$=\"#symbol\"],\n.json5e-deck.markdown-source-view span[src$=\"#portrait\"],\n.json5e-deck.markdown-source-view span[src$=\"#token\"],\n.json5e-deck.markdown-source-view span[src$=\"#right\"],\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-deck.markdown-source-view img[src$=\"#card\"],\n.json5e-deck.markdown-source-view img[src$=\"#symbol\"],\n.json5e-deck.markdown-source-view img[src$=\"#portrait\"],\n.json5e-deck.markdown-source-view img[src$=\"#token\"],\n.json5e-deck.markdown-source-view img[src$=\"#right\"],\n.json5e-deity.markdown-preview-view div[src$=\"#card\"],\n.json5e-deity.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-deity.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-deity.markdown-preview-view div[src$=\"#token\"],\n.json5e-deity.markdown-preview-view div[src$=\"#right\"],\n.json5e-deity.markdown-preview-view span[src$=\"#card\"],\n.json5e-deity.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-deity.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-deity.markdown-preview-view span[src$=\"#token\"],\n.json5e-deity.markdown-preview-view span[src$=\"#right\"],\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-deity.markdown-preview-view img[src$=\"#card\"],\n.json5e-deity.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-deity.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-deity.markdown-preview-view img[src$=\"#token\"],\n.json5e-deity.markdown-preview-view img[src$=\"#right\"],\n.json5e-deity.markdown-source-view div[src$=\"#card\"],\n.json5e-deity.markdown-source-view div[src$=\"#symbol\"],\n.json5e-deity.markdown-source-view div[src$=\"#portrait\"],\n.json5e-deity.markdown-source-view div[src$=\"#token\"],\n.json5e-deity.markdown-source-view div[src$=\"#right\"],\n.json5e-deity.markdown-source-view span[src$=\"#card\"],\n.json5e-deity.markdown-source-view span[src$=\"#symbol\"],\n.json5e-deity.markdown-source-view span[src$=\"#portrait\"],\n.json5e-deity.markdown-source-view span[src$=\"#token\"],\n.json5e-deity.markdown-source-view span[src$=\"#right\"],\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-deity.markdown-source-view img[src$=\"#card\"],\n.json5e-deity.markdown-source-view img[src$=\"#symbol\"],\n.json5e-deity.markdown-source-view img[src$=\"#portrait\"],\n.json5e-deity.markdown-source-view img[src$=\"#token\"],\n.json5e-deity.markdown-source-view img[src$=\"#right\"],\n.json5e-feat.markdown-preview-view div[src$=\"#card\"],\n.json5e-feat.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-feat.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-feat.markdown-preview-view div[src$=\"#token\"],\n.json5e-feat.markdown-preview-view div[src$=\"#right\"],\n.json5e-feat.markdown-preview-view span[src$=\"#card\"],\n.json5e-feat.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-feat.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-feat.markdown-preview-view span[src$=\"#token\"],\n.json5e-feat.markdown-preview-view span[src$=\"#right\"],\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-feat.markdown-preview-view img[src$=\"#card\"],\n.json5e-feat.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-feat.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-feat.markdown-preview-view img[src$=\"#token\"],\n.json5e-feat.markdown-preview-view img[src$=\"#right\"],\n.json5e-feat.markdown-source-view div[src$=\"#card\"],\n.json5e-feat.markdown-source-view div[src$=\"#symbol\"],\n.json5e-feat.markdown-source-view div[src$=\"#portrait\"],\n.json5e-feat.markdown-source-view div[src$=\"#token\"],\n.json5e-feat.markdown-source-view div[src$=\"#right\"],\n.json5e-feat.markdown-source-view span[src$=\"#card\"],\n.json5e-feat.markdown-source-view span[src$=\"#symbol\"],\n.json5e-feat.markdown-source-view span[src$=\"#portrait\"],\n.json5e-feat.markdown-source-view span[src$=\"#token\"],\n.json5e-feat.markdown-source-view span[src$=\"#right\"],\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-feat.markdown-source-view img[src$=\"#card\"],\n.json5e-feat.markdown-source-view img[src$=\"#symbol\"],\n.json5e-feat.markdown-source-view img[src$=\"#portrait\"],\n.json5e-feat.markdown-source-view img[src$=\"#token\"],\n.json5e-feat.markdown-source-view img[src$=\"#right\"],\n.json5e-hazard.markdown-preview-view div[src$=\"#card\"],\n.json5e-hazard.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-hazard.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-hazard.markdown-preview-view div[src$=\"#token\"],\n.json5e-hazard.markdown-preview-view div[src$=\"#right\"],\n.json5e-hazard.markdown-preview-view span[src$=\"#card\"],\n.json5e-hazard.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-hazard.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-hazard.markdown-preview-view span[src$=\"#token\"],\n.json5e-hazard.markdown-preview-view span[src$=\"#right\"],\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-hazard.markdown-preview-view img[src$=\"#card\"],\n.json5e-hazard.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-hazard.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-hazard.markdown-preview-view img[src$=\"#token\"],\n.json5e-hazard.markdown-preview-view img[src$=\"#right\"],\n.json5e-hazard.markdown-source-view div[src$=\"#card\"],\n.json5e-hazard.markdown-source-view div[src$=\"#symbol\"],\n.json5e-hazard.markdown-source-view div[src$=\"#portrait\"],\n.json5e-hazard.markdown-source-view div[src$=\"#token\"],\n.json5e-hazard.markdown-source-view div[src$=\"#right\"],\n.json5e-hazard.markdown-source-view span[src$=\"#card\"],\n.json5e-hazard.markdown-source-view span[src$=\"#symbol\"],\n.json5e-hazard.markdown-source-view span[src$=\"#portrait\"],\n.json5e-hazard.markdown-source-view span[src$=\"#token\"],\n.json5e-hazard.markdown-source-view span[src$=\"#right\"],\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-hazard.markdown-source-view img[src$=\"#card\"],\n.json5e-hazard.markdown-source-view img[src$=\"#symbol\"],\n.json5e-hazard.markdown-source-view img[src$=\"#portrait\"],\n.json5e-hazard.markdown-source-view img[src$=\"#token\"],\n.json5e-hazard.markdown-source-view img[src$=\"#right\"],\n.json5e-item.markdown-preview-view div[src$=\"#card\"],\n.json5e-item.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-item.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-item.markdown-preview-view div[src$=\"#token\"],\n.json5e-item.markdown-preview-view div[src$=\"#right\"],\n.json5e-item.markdown-preview-view span[src$=\"#card\"],\n.json5e-item.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-item.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-item.markdown-preview-view span[src$=\"#token\"],\n.json5e-item.markdown-preview-view span[src$=\"#right\"],\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-item.markdown-preview-view img[src$=\"#card\"],\n.json5e-item.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-item.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-item.markdown-preview-view img[src$=\"#token\"],\n.json5e-item.markdown-preview-view img[src$=\"#right\"],\n.json5e-item.markdown-source-view div[src$=\"#card\"],\n.json5e-item.markdown-source-view div[src$=\"#symbol\"],\n.json5e-item.markdown-source-view div[src$=\"#portrait\"],\n.json5e-item.markdown-source-view div[src$=\"#token\"],\n.json5e-item.markdown-source-view div[src$=\"#right\"],\n.json5e-item.markdown-source-view span[src$=\"#card\"],\n.json5e-item.markdown-source-view span[src$=\"#symbol\"],\n.json5e-item.markdown-source-view span[src$=\"#portrait\"],\n.json5e-item.markdown-source-view span[src$=\"#token\"],\n.json5e-item.markdown-source-view span[src$=\"#right\"],\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-item.markdown-source-view img[src$=\"#card\"],\n.json5e-item.markdown-source-view img[src$=\"#symbol\"],\n.json5e-item.markdown-source-view img[src$=\"#portrait\"],\n.json5e-item.markdown-source-view img[src$=\"#token\"],\n.json5e-item.markdown-source-view img[src$=\"#right\"],\n.json5e-monster.markdown-preview-view div[src$=\"#card\"],\n.json5e-monster.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-monster.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-monster.markdown-preview-view div[src$=\"#token\"],\n.json5e-monster.markdown-preview-view div[src$=\"#right\"],\n.json5e-monster.markdown-preview-view span[src$=\"#card\"],\n.json5e-monster.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-monster.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-monster.markdown-preview-view span[src$=\"#token\"],\n.json5e-monster.markdown-preview-view span[src$=\"#right\"],\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-monster.markdown-preview-view img[src$=\"#card\"],\n.json5e-monster.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-monster.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-monster.markdown-preview-view img[src$=\"#token\"],\n.json5e-monster.markdown-preview-view img[src$=\"#right\"],\n.json5e-monster.markdown-source-view div[src$=\"#card\"],\n.json5e-monster.markdown-source-view div[src$=\"#symbol\"],\n.json5e-monster.markdown-source-view div[src$=\"#portrait\"],\n.json5e-monster.markdown-source-view div[src$=\"#token\"],\n.json5e-monster.markdown-source-view div[src$=\"#right\"],\n.json5e-monster.markdown-source-view span[src$=\"#card\"],\n.json5e-monster.markdown-source-view span[src$=\"#symbol\"],\n.json5e-monster.markdown-source-view span[src$=\"#portrait\"],\n.json5e-monster.markdown-source-view span[src$=\"#token\"],\n.json5e-monster.markdown-source-view span[src$=\"#right\"],\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-monster.markdown-source-view img[src$=\"#card\"],\n.json5e-monster.markdown-source-view img[src$=\"#symbol\"],\n.json5e-monster.markdown-source-view img[src$=\"#portrait\"],\n.json5e-monster.markdown-source-view img[src$=\"#token\"],\n.json5e-monster.markdown-source-view img[src$=\"#right\"],\n.json5e-note.markdown-preview-view div[src$=\"#card\"],\n.json5e-note.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-note.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-note.markdown-preview-view div[src$=\"#token\"],\n.json5e-note.markdown-preview-view div[src$=\"#right\"],\n.json5e-note.markdown-preview-view span[src$=\"#card\"],\n.json5e-note.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-note.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-note.markdown-preview-view span[src$=\"#token\"],\n.json5e-note.markdown-preview-view span[src$=\"#right\"],\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-note.markdown-preview-view img[src$=\"#card\"],\n.json5e-note.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-note.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-note.markdown-preview-view img[src$=\"#token\"],\n.json5e-note.markdown-preview-view img[src$=\"#right\"],\n.json5e-note.markdown-source-view div[src$=\"#card\"],\n.json5e-note.markdown-source-view div[src$=\"#symbol\"],\n.json5e-note.markdown-source-view div[src$=\"#portrait\"],\n.json5e-note.markdown-source-view div[src$=\"#token\"],\n.json5e-note.markdown-source-view div[src$=\"#right\"],\n.json5e-note.markdown-source-view span[src$=\"#card\"],\n.json5e-note.markdown-source-view span[src$=\"#symbol\"],\n.json5e-note.markdown-source-view span[src$=\"#portrait\"],\n.json5e-note.markdown-source-view span[src$=\"#token\"],\n.json5e-note.markdown-source-view span[src$=\"#right\"],\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-note.markdown-source-view img[src$=\"#card\"],\n.json5e-note.markdown-source-view img[src$=\"#symbol\"],\n.json5e-note.markdown-source-view img[src$=\"#portrait\"],\n.json5e-note.markdown-source-view img[src$=\"#token\"],\n.json5e-note.markdown-source-view img[src$=\"#right\"],\n.json5e-object.markdown-preview-view div[src$=\"#card\"],\n.json5e-object.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-object.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-object.markdown-preview-view div[src$=\"#token\"],\n.json5e-object.markdown-preview-view div[src$=\"#right\"],\n.json5e-object.markdown-preview-view span[src$=\"#card\"],\n.json5e-object.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-object.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-object.markdown-preview-view span[src$=\"#token\"],\n.json5e-object.markdown-preview-view span[src$=\"#right\"],\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-object.markdown-preview-view img[src$=\"#card\"],\n.json5e-object.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-object.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-object.markdown-preview-view img[src$=\"#token\"],\n.json5e-object.markdown-preview-view img[src$=\"#right\"],\n.json5e-object.markdown-source-view div[src$=\"#card\"],\n.json5e-object.markdown-source-view div[src$=\"#symbol\"],\n.json5e-object.markdown-source-view div[src$=\"#portrait\"],\n.json5e-object.markdown-source-view div[src$=\"#token\"],\n.json5e-object.markdown-source-view div[src$=\"#right\"],\n.json5e-object.markdown-source-view span[src$=\"#card\"],\n.json5e-object.markdown-source-view span[src$=\"#symbol\"],\n.json5e-object.markdown-source-view span[src$=\"#portrait\"],\n.json5e-object.markdown-source-view span[src$=\"#token\"],\n.json5e-object.markdown-source-view span[src$=\"#right\"],\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-object.markdown-source-view img[src$=\"#card\"],\n.json5e-object.markdown-source-view img[src$=\"#symbol\"],\n.json5e-object.markdown-source-view img[src$=\"#portrait\"],\n.json5e-object.markdown-source-view img[src$=\"#token\"],\n.json5e-object.markdown-source-view img[src$=\"#right\"],\n.json5e-psionic.markdown-preview-view div[src$=\"#card\"],\n.json5e-psionic.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-psionic.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-psionic.markdown-preview-view div[src$=\"#token\"],\n.json5e-psionic.markdown-preview-view div[src$=\"#right\"],\n.json5e-psionic.markdown-preview-view span[src$=\"#card\"],\n.json5e-psionic.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-psionic.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-psionic.markdown-preview-view span[src$=\"#token\"],\n.json5e-psionic.markdown-preview-view span[src$=\"#right\"],\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-psionic.markdown-preview-view img[src$=\"#card\"],\n.json5e-psionic.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-psionic.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-psionic.markdown-preview-view img[src$=\"#token\"],\n.json5e-psionic.markdown-preview-view img[src$=\"#right\"],\n.json5e-psionic.markdown-source-view div[src$=\"#card\"],\n.json5e-psionic.markdown-source-view div[src$=\"#symbol\"],\n.json5e-psionic.markdown-source-view div[src$=\"#portrait\"],\n.json5e-psionic.markdown-source-view div[src$=\"#token\"],\n.json5e-psionic.markdown-source-view div[src$=\"#right\"],\n.json5e-psionic.markdown-source-view span[src$=\"#card\"],\n.json5e-psionic.markdown-source-view span[src$=\"#symbol\"],\n.json5e-psionic.markdown-source-view span[src$=\"#portrait\"],\n.json5e-psionic.markdown-source-view span[src$=\"#token\"],\n.json5e-psionic.markdown-source-view span[src$=\"#right\"],\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-psionic.markdown-source-view img[src$=\"#card\"],\n.json5e-psionic.markdown-source-view img[src$=\"#symbol\"],\n.json5e-psionic.markdown-source-view img[src$=\"#portrait\"],\n.json5e-psionic.markdown-source-view img[src$=\"#token\"],\n.json5e-psionic.markdown-source-view img[src$=\"#right\"],\n.json5e-race.markdown-preview-view div[src$=\"#card\"],\n.json5e-race.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-race.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-race.markdown-preview-view div[src$=\"#token\"],\n.json5e-race.markdown-preview-view div[src$=\"#right\"],\n.json5e-race.markdown-preview-view span[src$=\"#card\"],\n.json5e-race.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-race.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-race.markdown-preview-view span[src$=\"#token\"],\n.json5e-race.markdown-preview-view span[src$=\"#right\"],\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-race.markdown-preview-view img[src$=\"#card\"],\n.json5e-race.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-race.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-race.markdown-preview-view img[src$=\"#token\"],\n.json5e-race.markdown-preview-view img[src$=\"#right\"],\n.json5e-race.markdown-source-view div[src$=\"#card\"],\n.json5e-race.markdown-source-view div[src$=\"#symbol\"],\n.json5e-race.markdown-source-view div[src$=\"#portrait\"],\n.json5e-race.markdown-source-view div[src$=\"#token\"],\n.json5e-race.markdown-source-view div[src$=\"#right\"],\n.json5e-race.markdown-source-view span[src$=\"#card\"],\n.json5e-race.markdown-source-view span[src$=\"#symbol\"],\n.json5e-race.markdown-source-view span[src$=\"#portrait\"],\n.json5e-race.markdown-source-view span[src$=\"#token\"],\n.json5e-race.markdown-source-view span[src$=\"#right\"],\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-race.markdown-source-view img[src$=\"#card\"],\n.json5e-race.markdown-source-view img[src$=\"#symbol\"],\n.json5e-race.markdown-source-view img[src$=\"#portrait\"],\n.json5e-race.markdown-source-view img[src$=\"#token\"],\n.json5e-race.markdown-source-view img[src$=\"#right\"],\n.json5e-reward.markdown-preview-view div[src$=\"#card\"],\n.json5e-reward.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-reward.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-reward.markdown-preview-view div[src$=\"#token\"],\n.json5e-reward.markdown-preview-view div[src$=\"#right\"],\n.json5e-reward.markdown-preview-view span[src$=\"#card\"],\n.json5e-reward.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-reward.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-reward.markdown-preview-view span[src$=\"#token\"],\n.json5e-reward.markdown-preview-view span[src$=\"#right\"],\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-reward.markdown-preview-view img[src$=\"#card\"],\n.json5e-reward.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-reward.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-reward.markdown-preview-view img[src$=\"#token\"],\n.json5e-reward.markdown-preview-view img[src$=\"#right\"],\n.json5e-reward.markdown-source-view div[src$=\"#card\"],\n.json5e-reward.markdown-source-view div[src$=\"#symbol\"],\n.json5e-reward.markdown-source-view div[src$=\"#portrait\"],\n.json5e-reward.markdown-source-view div[src$=\"#token\"],\n.json5e-reward.markdown-source-view div[src$=\"#right\"],\n.json5e-reward.markdown-source-view span[src$=\"#card\"],\n.json5e-reward.markdown-source-view span[src$=\"#symbol\"],\n.json5e-reward.markdown-source-view span[src$=\"#portrait\"],\n.json5e-reward.markdown-source-view span[src$=\"#token\"],\n.json5e-reward.markdown-source-view span[src$=\"#right\"],\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-reward.markdown-source-view img[src$=\"#card\"],\n.json5e-reward.markdown-source-view img[src$=\"#symbol\"],\n.json5e-reward.markdown-source-view img[src$=\"#portrait\"],\n.json5e-reward.markdown-source-view img[src$=\"#token\"],\n.json5e-reward.markdown-source-view img[src$=\"#right\"],\n.json5e-species.markdown-preview-view div[src$=\"#card\"],\n.json5e-species.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-species.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-species.markdown-preview-view div[src$=\"#token\"],\n.json5e-species.markdown-preview-view div[src$=\"#right\"],\n.json5e-species.markdown-preview-view span[src$=\"#card\"],\n.json5e-species.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-species.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-species.markdown-preview-view span[src$=\"#token\"],\n.json5e-species.markdown-preview-view span[src$=\"#right\"],\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-species.markdown-preview-view img[src$=\"#card\"],\n.json5e-species.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-species.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-species.markdown-preview-view img[src$=\"#token\"],\n.json5e-species.markdown-preview-view img[src$=\"#right\"],\n.json5e-species.markdown-source-view div[src$=\"#card\"],\n.json5e-species.markdown-source-view div[src$=\"#symbol\"],\n.json5e-species.markdown-source-view div[src$=\"#portrait\"],\n.json5e-species.markdown-source-view div[src$=\"#token\"],\n.json5e-species.markdown-source-view div[src$=\"#right\"],\n.json5e-species.markdown-source-view span[src$=\"#card\"],\n.json5e-species.markdown-source-view span[src$=\"#symbol\"],\n.json5e-species.markdown-source-view span[src$=\"#portrait\"],\n.json5e-species.markdown-source-view span[src$=\"#token\"],\n.json5e-species.markdown-source-view span[src$=\"#right\"],\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-species.markdown-source-view img[src$=\"#card\"],\n.json5e-species.markdown-source-view img[src$=\"#symbol\"],\n.json5e-species.markdown-source-view img[src$=\"#portrait\"],\n.json5e-species.markdown-source-view img[src$=\"#token\"],\n.json5e-species.markdown-source-view img[src$=\"#right\"],\n.json5e-spell.markdown-preview-view div[src$=\"#card\"],\n.json5e-spell.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-spell.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-spell.markdown-preview-view div[src$=\"#token\"],\n.json5e-spell.markdown-preview-view div[src$=\"#right\"],\n.json5e-spell.markdown-preview-view span[src$=\"#card\"],\n.json5e-spell.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-spell.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-spell.markdown-preview-view span[src$=\"#token\"],\n.json5e-spell.markdown-preview-view span[src$=\"#right\"],\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-spell.markdown-preview-view img[src$=\"#card\"],\n.json5e-spell.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-spell.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-spell.markdown-preview-view img[src$=\"#token\"],\n.json5e-spell.markdown-preview-view img[src$=\"#right\"],\n.json5e-spell.markdown-source-view div[src$=\"#card\"],\n.json5e-spell.markdown-source-view div[src$=\"#symbol\"],\n.json5e-spell.markdown-source-view div[src$=\"#portrait\"],\n.json5e-spell.markdown-source-view div[src$=\"#token\"],\n.json5e-spell.markdown-source-view div[src$=\"#right\"],\n.json5e-spell.markdown-source-view span[src$=\"#card\"],\n.json5e-spell.markdown-source-view span[src$=\"#symbol\"],\n.json5e-spell.markdown-source-view span[src$=\"#portrait\"],\n.json5e-spell.markdown-source-view span[src$=\"#token\"],\n.json5e-spell.markdown-source-view span[src$=\"#right\"],\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-spell.markdown-source-view img[src$=\"#card\"],\n.json5e-spell.markdown-source-view img[src$=\"#symbol\"],\n.json5e-spell.markdown-source-view img[src$=\"#portrait\"],\n.json5e-spell.markdown-source-view img[src$=\"#token\"],\n.json5e-spell.markdown-source-view img[src$=\"#right\"],\n.json5e-vehicle.markdown-preview-view div[src$=\"#card\"],\n.json5e-vehicle.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-vehicle.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-vehicle.markdown-preview-view div[src$=\"#token\"],\n.json5e-vehicle.markdown-preview-view div[src$=\"#right\"],\n.json5e-vehicle.markdown-preview-view span[src$=\"#card\"],\n.json5e-vehicle.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-vehicle.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-vehicle.markdown-preview-view span[src$=\"#token\"],\n.json5e-vehicle.markdown-preview-view span[src$=\"#right\"],\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-vehicle.markdown-preview-view img[src$=\"#card\"],\n.json5e-vehicle.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-vehicle.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-vehicle.markdown-preview-view img[src$=\"#token\"],\n.json5e-vehicle.markdown-preview-view img[src$=\"#right\"],\n.json5e-vehicle.markdown-source-view div[src$=\"#card\"],\n.json5e-vehicle.markdown-source-view div[src$=\"#symbol\"],\n.json5e-vehicle.markdown-source-view div[src$=\"#portrait\"],\n.json5e-vehicle.markdown-source-view div[src$=\"#token\"],\n.json5e-vehicle.markdown-source-view div[src$=\"#right\"],\n.json5e-vehicle.markdown-source-view span[src$=\"#card\"],\n.json5e-vehicle.markdown-source-view span[src$=\"#symbol\"],\n.json5e-vehicle.markdown-source-view span[src$=\"#portrait\"],\n.json5e-vehicle.markdown-source-view span[src$=\"#token\"],\n.json5e-vehicle.markdown-source-view span[src$=\"#right\"],\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-vehicle.markdown-source-view img[src$=\"#card\"],\n.json5e-vehicle.markdown-source-view img[src$=\"#symbol\"],\n.json5e-vehicle.markdown-source-view img[src$=\"#portrait\"],\n.json5e-vehicle.markdown-source-view img[src$=\"#token\"],\n.json5e-vehicle.markdown-source-view img[src$=\"#right\"] {\n  float: right;\n  padding-left: 5px;\n}\n.json5e-background.markdown-preview-view div[src$=\"#left\"],\n.json5e-background.markdown-preview-view span[src$=\"#left\"],\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-background.markdown-preview-view img[src$=\"#left\"], .json5e-background.markdown-source-view div[src$=\"#left\"],\n.json5e-background.markdown-source-view span[src$=\"#left\"],\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-background.markdown-source-view img[src$=\"#left\"],\n.json5e-class.markdown-preview-view div[src$=\"#left\"],\n.json5e-class.markdown-preview-view span[src$=\"#left\"],\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-class.markdown-preview-view img[src$=\"#left\"],\n.json5e-class.markdown-source-view div[src$=\"#left\"],\n.json5e-class.markdown-source-view span[src$=\"#left\"],\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-class.markdown-source-view img[src$=\"#left\"],\n.json5e-deck.markdown-preview-view div[src$=\"#left\"],\n.json5e-deck.markdown-preview-view span[src$=\"#left\"],\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-deck.markdown-preview-view img[src$=\"#left\"],\n.json5e-deck.markdown-source-view div[src$=\"#left\"],\n.json5e-deck.markdown-source-view span[src$=\"#left\"],\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-deck.markdown-source-view img[src$=\"#left\"],\n.json5e-deity.markdown-preview-view div[src$=\"#left\"],\n.json5e-deity.markdown-preview-view span[src$=\"#left\"],\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-deity.markdown-preview-view img[src$=\"#left\"],\n.json5e-deity.markdown-source-view div[src$=\"#left\"],\n.json5e-deity.markdown-source-view span[src$=\"#left\"],\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-deity.markdown-source-view img[src$=\"#left\"],\n.json5e-feat.markdown-preview-view div[src$=\"#left\"],\n.json5e-feat.markdown-preview-view span[src$=\"#left\"],\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-feat.markdown-preview-view img[src$=\"#left\"],\n.json5e-feat.markdown-source-view div[src$=\"#left\"],\n.json5e-feat.markdown-source-view span[src$=\"#left\"],\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-feat.markdown-source-view img[src$=\"#left\"],\n.json5e-hazard.markdown-preview-view div[src$=\"#left\"],\n.json5e-hazard.markdown-preview-view span[src$=\"#left\"],\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-hazard.markdown-preview-view img[src$=\"#left\"],\n.json5e-hazard.markdown-source-view div[src$=\"#left\"],\n.json5e-hazard.markdown-source-view span[src$=\"#left\"],\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-hazard.markdown-source-view img[src$=\"#left\"],\n.json5e-item.markdown-preview-view div[src$=\"#left\"],\n.json5e-item.markdown-preview-view span[src$=\"#left\"],\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-item.markdown-preview-view img[src$=\"#left\"],\n.json5e-item.markdown-source-view div[src$=\"#left\"],\n.json5e-item.markdown-source-view span[src$=\"#left\"],\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-item.markdown-source-view img[src$=\"#left\"],\n.json5e-monster.markdown-preview-view div[src$=\"#left\"],\n.json5e-monster.markdown-preview-view span[src$=\"#left\"],\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-monster.markdown-preview-view img[src$=\"#left\"],\n.json5e-monster.markdown-source-view div[src$=\"#left\"],\n.json5e-monster.markdown-source-view span[src$=\"#left\"],\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-monster.markdown-source-view img[src$=\"#left\"],\n.json5e-note.markdown-preview-view div[src$=\"#left\"],\n.json5e-note.markdown-preview-view span[src$=\"#left\"],\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-note.markdown-preview-view img[src$=\"#left\"],\n.json5e-note.markdown-source-view div[src$=\"#left\"],\n.json5e-note.markdown-source-view span[src$=\"#left\"],\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-note.markdown-source-view img[src$=\"#left\"],\n.json5e-object.markdown-preview-view div[src$=\"#left\"],\n.json5e-object.markdown-preview-view span[src$=\"#left\"],\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-object.markdown-preview-view img[src$=\"#left\"],\n.json5e-object.markdown-source-view div[src$=\"#left\"],\n.json5e-object.markdown-source-view span[src$=\"#left\"],\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-object.markdown-source-view img[src$=\"#left\"],\n.json5e-psionic.markdown-preview-view div[src$=\"#left\"],\n.json5e-psionic.markdown-preview-view span[src$=\"#left\"],\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-psionic.markdown-preview-view img[src$=\"#left\"],\n.json5e-psionic.markdown-source-view div[src$=\"#left\"],\n.json5e-psionic.markdown-source-view span[src$=\"#left\"],\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-psionic.markdown-source-view img[src$=\"#left\"],\n.json5e-race.markdown-preview-view div[src$=\"#left\"],\n.json5e-race.markdown-preview-view span[src$=\"#left\"],\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-race.markdown-preview-view img[src$=\"#left\"],\n.json5e-race.markdown-source-view div[src$=\"#left\"],\n.json5e-race.markdown-source-view span[src$=\"#left\"],\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-race.markdown-source-view img[src$=\"#left\"],\n.json5e-reward.markdown-preview-view div[src$=\"#left\"],\n.json5e-reward.markdown-preview-view span[src$=\"#left\"],\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-reward.markdown-preview-view img[src$=\"#left\"],\n.json5e-reward.markdown-source-view div[src$=\"#left\"],\n.json5e-reward.markdown-source-view span[src$=\"#left\"],\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-reward.markdown-source-view img[src$=\"#left\"],\n.json5e-species.markdown-preview-view div[src$=\"#left\"],\n.json5e-species.markdown-preview-view span[src$=\"#left\"],\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-species.markdown-preview-view img[src$=\"#left\"],\n.json5e-species.markdown-source-view div[src$=\"#left\"],\n.json5e-species.markdown-source-view span[src$=\"#left\"],\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-species.markdown-source-view img[src$=\"#left\"],\n.json5e-spell.markdown-preview-view div[src$=\"#left\"],\n.json5e-spell.markdown-preview-view span[src$=\"#left\"],\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-spell.markdown-preview-view img[src$=\"#left\"],\n.json5e-spell.markdown-source-view div[src$=\"#left\"],\n.json5e-spell.markdown-source-view span[src$=\"#left\"],\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-spell.markdown-source-view img[src$=\"#left\"],\n.json5e-vehicle.markdown-preview-view div[src$=\"#left\"],\n.json5e-vehicle.markdown-preview-view span[src$=\"#left\"],\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-vehicle.markdown-preview-view img[src$=\"#left\"],\n.json5e-vehicle.markdown-source-view div[src$=\"#left\"],\n.json5e-vehicle.markdown-source-view span[src$=\"#left\"],\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-vehicle.markdown-source-view img[src$=\"#left\"] {\n  float: left;\n  padding-right: 5px;\n}\n.json5e-background.markdown-preview-view div[src$=\"#left\"] img, .json5e-background.markdown-preview-view div[src$=\"#center\"] img, .json5e-background.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-background.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-background.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-background.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-background.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-background.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-background.markdown-preview-view img[src$=\"#right\"] img, .json5e-background.markdown-source-view div[src$=\"#left\"] img, .json5e-background.markdown-source-view div[src$=\"#center\"] img, .json5e-background.markdown-source-view div[src$=\"#right\"] img,\n.json5e-background.markdown-source-view span[src$=\"#left\"] img,\n.json5e-background.markdown-source-view span[src$=\"#center\"] img,\n.json5e-background.markdown-source-view span[src$=\"#right\"] img,\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-background.markdown-source-view img[src$=\"#left\"] img,\n.json5e-background.markdown-source-view img[src$=\"#center\"] img,\n.json5e-background.markdown-source-view img[src$=\"#right\"] img,\n.json5e-class.markdown-preview-view div[src$=\"#left\"] img,\n.json5e-class.markdown-preview-view div[src$=\"#center\"] img,\n.json5e-class.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-class.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-class.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-class.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-class.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-class.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-class.markdown-preview-view img[src$=\"#right\"] img,\n.json5e-class.markdown-source-view div[src$=\"#left\"] img,\n.json5e-class.markdown-source-view div[src$=\"#center\"] img,\n.json5e-class.markdown-source-view div[src$=\"#right\"] img,\n.json5e-class.markdown-source-view span[src$=\"#left\"] img,\n.json5e-class.markdown-source-view span[src$=\"#center\"] img,\n.json5e-class.markdown-source-view span[src$=\"#right\"] img,\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-class.markdown-source-view img[src$=\"#left\"] img,\n.json5e-class.markdown-source-view img[src$=\"#center\"] img,\n.json5e-class.markdown-source-view img[src$=\"#right\"] img,\n.json5e-deck.markdown-preview-view div[src$=\"#left\"] img,\n.json5e-deck.markdown-preview-view div[src$=\"#center\"] img,\n.json5e-deck.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-deck.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-deck.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-deck.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-deck.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-deck.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-deck.markdown-preview-view img[src$=\"#right\"] img,\n.json5e-deck.markdown-source-view div[src$=\"#left\"] img,\n.json5e-deck.markdown-source-view div[src$=\"#center\"] img,\n.json5e-deck.markdown-source-view div[src$=\"#right\"] img,\n.json5e-deck.markdown-source-view span[src$=\"#left\"] img,\n.json5e-deck.markdown-source-view span[src$=\"#center\"] img,\n.json5e-deck.markdown-source-view span[src$=\"#right\"] img,\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-deck.markdown-source-view img[src$=\"#left\"] img,\n.json5e-deck.markdown-source-view img[src$=\"#center\"] img,\n.json5e-deck.markdown-source-view img[src$=\"#right\"] img,\n.json5e-deity.markdown-preview-view div[src$=\"#left\"] img,\n.json5e-deity.markdown-preview-view div[src$=\"#center\"] img,\n.json5e-deity.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-deity.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-deity.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-deity.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-deity.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-deity.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-deity.markdown-preview-view img[src$=\"#right\"] img,\n.json5e-deity.markdown-source-view div[src$=\"#left\"] img,\n.json5e-deity.markdown-source-view div[src$=\"#center\"] img,\n.json5e-deity.markdown-source-view div[src$=\"#right\"] img,\n.json5e-deity.markdown-source-view span[src$=\"#left\"] img,\n.json5e-deity.markdown-source-view span[src$=\"#center\"] img,\n.json5e-deity.markdown-source-view span[src$=\"#right\"] img,\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-deity.markdown-source-view img[src$=\"#left\"] img,\n.json5e-deity.markdown-source-view img[src$=\"#center\"] img,\n.json5e-deity.markdown-source-view img[src$=\"#right\"] img,\n.json5e-feat.markdown-preview-view div[src$=\"#left\"] img,\n.json5e-feat.markdown-preview-view div[src$=\"#center\"] img,\n.json5e-feat.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-feat.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-feat.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-feat.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-feat.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-feat.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-feat.markdown-preview-view img[src$=\"#right\"] img,\n.json5e-feat.markdown-source-view div[src$=\"#left\"] img,\n.json5e-feat.markdown-source-view div[src$=\"#center\"] img,\n.json5e-feat.markdown-source-view div[src$=\"#right\"] img,\n.json5e-feat.markdown-source-view span[src$=\"#left\"] img,\n.json5e-feat.markdown-source-view span[src$=\"#center\"] img,\n.json5e-feat.markdown-source-view span[src$=\"#right\"] img,\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-feat.markdown-source-view img[src$=\"#left\"] img,\n.json5e-feat.markdown-source-view img[src$=\"#center\"] img,\n.json5e-feat.markdown-source-view img[src$=\"#right\"] img,\n.json5e-hazard.markdown-preview-view div[src$=\"#left\"] img,\n.json5e-hazard.markdown-preview-view div[src$=\"#center\"] img,\n.json5e-hazard.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-hazard.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-hazard.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-hazard.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-hazard.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-hazard.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-hazard.markdown-preview-view img[src$=\"#right\"] img,\n.json5e-hazard.markdown-source-view div[src$=\"#left\"] img,\n.json5e-hazard.markdown-source-view div[src$=\"#center\"] img,\n.json5e-hazard.markdown-source-view div[src$=\"#right\"] img,\n.json5e-hazard.markdown-source-view span[src$=\"#left\"] img,\n.json5e-hazard.markdown-source-view span[src$=\"#center\"] img,\n.json5e-hazard.markdown-source-view span[src$=\"#right\"] img,\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-hazard.markdown-source-view img[src$=\"#left\"] img,\n.json5e-hazard.markdown-source-view img[src$=\"#center\"] img,\n.json5e-hazard.markdown-source-view img[src$=\"#right\"] img,\n.json5e-item.markdown-preview-view div[src$=\"#left\"] img,\n.json5e-item.markdown-preview-view div[src$=\"#center\"] img,\n.json5e-item.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-item.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-item.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-item.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-item.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-item.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-item.markdown-preview-view img[src$=\"#right\"] img,\n.json5e-item.markdown-source-view div[src$=\"#left\"] img,\n.json5e-item.markdown-source-view div[src$=\"#center\"] img,\n.json5e-item.markdown-source-view div[src$=\"#right\"] img,\n.json5e-item.markdown-source-view span[src$=\"#left\"] img,\n.json5e-item.markdown-source-view span[src$=\"#center\"] img,\n.json5e-item.markdown-source-view span[src$=\"#right\"] img,\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-item.markdown-source-view img[src$=\"#left\"] img,\n.json5e-item.markdown-source-view img[src$=\"#center\"] img,\n.json5e-item.markdown-source-view img[src$=\"#right\"] img,\n.json5e-monster.markdown-preview-view div[src$=\"#left\"] img,\n.json5e-monster.markdown-preview-view div[src$=\"#center\"] img,\n.json5e-monster.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-monster.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-monster.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-monster.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-monster.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-monster.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-monster.markdown-preview-view img[src$=\"#right\"] img,\n.json5e-monster.markdown-source-view div[src$=\"#left\"] img,\n.json5e-monster.markdown-source-view div[src$=\"#center\"] img,\n.json5e-monster.markdown-source-view div[src$=\"#right\"] img,\n.json5e-monster.markdown-source-view span[src$=\"#left\"] img,\n.json5e-monster.markdown-source-view span[src$=\"#center\"] img,\n.json5e-monster.markdown-source-view span[src$=\"#right\"] img,\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-monster.markdown-source-view img[src$=\"#left\"] img,\n.json5e-monster.markdown-source-view img[src$=\"#center\"] img,\n.json5e-monster.markdown-source-view img[src$=\"#right\"] img,\n.json5e-note.markdown-preview-view div[src$=\"#left\"] img,\n.json5e-note.markdown-preview-view div[src$=\"#center\"] img,\n.json5e-note.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-note.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-note.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-note.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-note.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-note.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-note.markdown-preview-view img[src$=\"#right\"] img,\n.json5e-note.markdown-source-view div[src$=\"#left\"] img,\n.json5e-note.markdown-source-view div[src$=\"#center\"] img,\n.json5e-note.markdown-source-view div[src$=\"#right\"] img,\n.json5e-note.markdown-source-view span[src$=\"#left\"] img,\n.json5e-note.markdown-source-view span[src$=\"#center\"] img,\n.json5e-note.markdown-source-view span[src$=\"#right\"] img,\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-note.markdown-source-view img[src$=\"#left\"] img,\n.json5e-note.markdown-source-view img[src$=\"#center\"] img,\n.json5e-note.markdown-source-view img[src$=\"#right\"] img,\n.json5e-object.markdown-preview-view div[src$=\"#left\"] img,\n.json5e-object.markdown-preview-view div[src$=\"#center\"] img,\n.json5e-object.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-object.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-object.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-object.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-object.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-object.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-object.markdown-preview-view img[src$=\"#right\"] img,\n.json5e-object.markdown-source-view div[src$=\"#left\"] img,\n.json5e-object.markdown-source-view div[src$=\"#center\"] img,\n.json5e-object.markdown-source-view div[src$=\"#right\"] img,\n.json5e-object.markdown-source-view span[src$=\"#left\"] img,\n.json5e-object.markdown-source-view span[src$=\"#center\"] img,\n.json5e-object.markdown-source-view span[src$=\"#right\"] img,\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-object.markdown-source-view img[src$=\"#left\"] img,\n.json5e-object.markdown-source-view img[src$=\"#center\"] img,\n.json5e-object.markdown-source-view img[src$=\"#right\"] img,\n.json5e-psionic.markdown-preview-view div[src$=\"#left\"] img,\n.json5e-psionic.markdown-preview-view div[src$=\"#center\"] img,\n.json5e-psionic.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-psionic.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-psionic.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-psionic.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-psionic.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-psionic.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-psionic.markdown-preview-view img[src$=\"#right\"] img,\n.json5e-psionic.markdown-source-view div[src$=\"#left\"] img,\n.json5e-psionic.markdown-source-view div[src$=\"#center\"] img,\n.json5e-psionic.markdown-source-view div[src$=\"#right\"] img,\n.json5e-psionic.markdown-source-view span[src$=\"#left\"] img,\n.json5e-psionic.markdown-source-view span[src$=\"#center\"] img,\n.json5e-psionic.markdown-source-view span[src$=\"#right\"] img,\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-psionic.markdown-source-view img[src$=\"#left\"] img,\n.json5e-psionic.markdown-source-view img[src$=\"#center\"] img,\n.json5e-psionic.markdown-source-view img[src$=\"#right\"] img,\n.json5e-race.markdown-preview-view div[src$=\"#left\"] img,\n.json5e-race.markdown-preview-view div[src$=\"#center\"] img,\n.json5e-race.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-race.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-race.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-race.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-race.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-race.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-race.markdown-preview-view img[src$=\"#right\"] img,\n.json5e-race.markdown-source-view div[src$=\"#left\"] img,\n.json5e-race.markdown-source-view div[src$=\"#center\"] img,\n.json5e-race.markdown-source-view div[src$=\"#right\"] img,\n.json5e-race.markdown-source-view span[src$=\"#left\"] img,\n.json5e-race.markdown-source-view span[src$=\"#center\"] img,\n.json5e-race.markdown-source-view span[src$=\"#right\"] img,\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-race.markdown-source-view img[src$=\"#left\"] img,\n.json5e-race.markdown-source-view img[src$=\"#center\"] img,\n.json5e-race.markdown-source-view img[src$=\"#right\"] img,\n.json5e-reward.markdown-preview-view div[src$=\"#left\"] img,\n.json5e-reward.markdown-preview-view div[src$=\"#center\"] img,\n.json5e-reward.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-reward.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-reward.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-reward.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-reward.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-reward.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-reward.markdown-preview-view img[src$=\"#right\"] img,\n.json5e-reward.markdown-source-view div[src$=\"#left\"] img,\n.json5e-reward.markdown-source-view div[src$=\"#center\"] img,\n.json5e-reward.markdown-source-view div[src$=\"#right\"] img,\n.json5e-reward.markdown-source-view span[src$=\"#left\"] img,\n.json5e-reward.markdown-source-view span[src$=\"#center\"] img,\n.json5e-reward.markdown-source-view span[src$=\"#right\"] img,\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-reward.markdown-source-view img[src$=\"#left\"] img,\n.json5e-reward.markdown-source-view img[src$=\"#center\"] img,\n.json5e-reward.markdown-source-view img[src$=\"#right\"] img,\n.json5e-species.markdown-preview-view div[src$=\"#left\"] img,\n.json5e-species.markdown-preview-view div[src$=\"#center\"] img,\n.json5e-species.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-species.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-species.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-species.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-species.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-species.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-species.markdown-preview-view img[src$=\"#right\"] img,\n.json5e-species.markdown-source-view div[src$=\"#left\"] img,\n.json5e-species.markdown-source-view div[src$=\"#center\"] img,\n.json5e-species.markdown-source-view div[src$=\"#right\"] img,\n.json5e-species.markdown-source-view span[src$=\"#left\"] img,\n.json5e-species.markdown-source-view span[src$=\"#center\"] img,\n.json5e-species.markdown-source-view span[src$=\"#right\"] img,\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-species.markdown-source-view img[src$=\"#left\"] img,\n.json5e-species.markdown-source-view img[src$=\"#center\"] img,\n.json5e-species.markdown-source-view img[src$=\"#right\"] img,\n.json5e-spell.markdown-preview-view div[src$=\"#left\"] img,\n.json5e-spell.markdown-preview-view div[src$=\"#center\"] img,\n.json5e-spell.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-spell.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-spell.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-spell.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-spell.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-spell.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-spell.markdown-preview-view img[src$=\"#right\"] img,\n.json5e-spell.markdown-source-view div[src$=\"#left\"] img,\n.json5e-spell.markdown-source-view div[src$=\"#center\"] img,\n.json5e-spell.markdown-source-view div[src$=\"#right\"] img,\n.json5e-spell.markdown-source-view span[src$=\"#left\"] img,\n.json5e-spell.markdown-source-view span[src$=\"#center\"] img,\n.json5e-spell.markdown-source-view span[src$=\"#right\"] img,\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-spell.markdown-source-view img[src$=\"#left\"] img,\n.json5e-spell.markdown-source-view img[src$=\"#center\"] img,\n.json5e-spell.markdown-source-view img[src$=\"#right\"] img,\n.json5e-vehicle.markdown-preview-view div[src$=\"#left\"] img,\n.json5e-vehicle.markdown-preview-view div[src$=\"#center\"] img,\n.json5e-vehicle.markdown-preview-view div[src$=\"#right\"] img,\n.json5e-vehicle.markdown-preview-view span[src$=\"#left\"] img,\n.json5e-vehicle.markdown-preview-view span[src$=\"#center\"] img,\n.json5e-vehicle.markdown-preview-view span[src$=\"#right\"] img,\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#left\"] img,\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#center\"] img,\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#right\"] img,\n.json5e-vehicle.markdown-preview-view img[src$=\"#left\"] img,\n.json5e-vehicle.markdown-preview-view img[src$=\"#center\"] img,\n.json5e-vehicle.markdown-preview-view img[src$=\"#right\"] img,\n.json5e-vehicle.markdown-source-view div[src$=\"#left\"] img,\n.json5e-vehicle.markdown-source-view div[src$=\"#center\"] img,\n.json5e-vehicle.markdown-source-view div[src$=\"#right\"] img,\n.json5e-vehicle.markdown-source-view span[src$=\"#left\"] img,\n.json5e-vehicle.markdown-source-view span[src$=\"#center\"] img,\n.json5e-vehicle.markdown-source-view span[src$=\"#right\"] img,\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#left\"] img,\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#center\"] img,\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#right\"] img,\n.json5e-vehicle.markdown-source-view img[src$=\"#left\"] img,\n.json5e-vehicle.markdown-source-view img[src$=\"#center\"] img,\n.json5e-vehicle.markdown-source-view img[src$=\"#right\"] img {\n  max-height: 60vh;\n}\n.json5e-background.markdown-preview-view div[src$=\"#left\"], .json5e-background.markdown-preview-view div[src$=\"#right\"],\n.json5e-background.markdown-preview-view span[src$=\"#left\"],\n.json5e-background.markdown-preview-view span[src$=\"#right\"],\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-background.markdown-preview-view img[src$=\"#left\"],\n.json5e-background.markdown-preview-view img[src$=\"#right\"], .json5e-background.markdown-source-view div[src$=\"#left\"], .json5e-background.markdown-source-view div[src$=\"#right\"],\n.json5e-background.markdown-source-view span[src$=\"#left\"],\n.json5e-background.markdown-source-view span[src$=\"#right\"],\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-background.markdown-source-view img[src$=\"#left\"],\n.json5e-background.markdown-source-view img[src$=\"#right\"],\n.json5e-class.markdown-preview-view div[src$=\"#left\"],\n.json5e-class.markdown-preview-view div[src$=\"#right\"],\n.json5e-class.markdown-preview-view span[src$=\"#left\"],\n.json5e-class.markdown-preview-view span[src$=\"#right\"],\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-class.markdown-preview-view img[src$=\"#left\"],\n.json5e-class.markdown-preview-view img[src$=\"#right\"],\n.json5e-class.markdown-source-view div[src$=\"#left\"],\n.json5e-class.markdown-source-view div[src$=\"#right\"],\n.json5e-class.markdown-source-view span[src$=\"#left\"],\n.json5e-class.markdown-source-view span[src$=\"#right\"],\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-class.markdown-source-view img[src$=\"#left\"],\n.json5e-class.markdown-source-view img[src$=\"#right\"],\n.json5e-deck.markdown-preview-view div[src$=\"#left\"],\n.json5e-deck.markdown-preview-view div[src$=\"#right\"],\n.json5e-deck.markdown-preview-view span[src$=\"#left\"],\n.json5e-deck.markdown-preview-view span[src$=\"#right\"],\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-deck.markdown-preview-view img[src$=\"#left\"],\n.json5e-deck.markdown-preview-view img[src$=\"#right\"],\n.json5e-deck.markdown-source-view div[src$=\"#left\"],\n.json5e-deck.markdown-source-view div[src$=\"#right\"],\n.json5e-deck.markdown-source-view span[src$=\"#left\"],\n.json5e-deck.markdown-source-view span[src$=\"#right\"],\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-deck.markdown-source-view img[src$=\"#left\"],\n.json5e-deck.markdown-source-view img[src$=\"#right\"],\n.json5e-deity.markdown-preview-view div[src$=\"#left\"],\n.json5e-deity.markdown-preview-view div[src$=\"#right\"],\n.json5e-deity.markdown-preview-view span[src$=\"#left\"],\n.json5e-deity.markdown-preview-view span[src$=\"#right\"],\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-deity.markdown-preview-view img[src$=\"#left\"],\n.json5e-deity.markdown-preview-view img[src$=\"#right\"],\n.json5e-deity.markdown-source-view div[src$=\"#left\"],\n.json5e-deity.markdown-source-view div[src$=\"#right\"],\n.json5e-deity.markdown-source-view span[src$=\"#left\"],\n.json5e-deity.markdown-source-view span[src$=\"#right\"],\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-deity.markdown-source-view img[src$=\"#left\"],\n.json5e-deity.markdown-source-view img[src$=\"#right\"],\n.json5e-feat.markdown-preview-view div[src$=\"#left\"],\n.json5e-feat.markdown-preview-view div[src$=\"#right\"],\n.json5e-feat.markdown-preview-view span[src$=\"#left\"],\n.json5e-feat.markdown-preview-view span[src$=\"#right\"],\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-feat.markdown-preview-view img[src$=\"#left\"],\n.json5e-feat.markdown-preview-view img[src$=\"#right\"],\n.json5e-feat.markdown-source-view div[src$=\"#left\"],\n.json5e-feat.markdown-source-view div[src$=\"#right\"],\n.json5e-feat.markdown-source-view span[src$=\"#left\"],\n.json5e-feat.markdown-source-view span[src$=\"#right\"],\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-feat.markdown-source-view img[src$=\"#left\"],\n.json5e-feat.markdown-source-view img[src$=\"#right\"],\n.json5e-hazard.markdown-preview-view div[src$=\"#left\"],\n.json5e-hazard.markdown-preview-view div[src$=\"#right\"],\n.json5e-hazard.markdown-preview-view span[src$=\"#left\"],\n.json5e-hazard.markdown-preview-view span[src$=\"#right\"],\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-hazard.markdown-preview-view img[src$=\"#left\"],\n.json5e-hazard.markdown-preview-view img[src$=\"#right\"],\n.json5e-hazard.markdown-source-view div[src$=\"#left\"],\n.json5e-hazard.markdown-source-view div[src$=\"#right\"],\n.json5e-hazard.markdown-source-view span[src$=\"#left\"],\n.json5e-hazard.markdown-source-view span[src$=\"#right\"],\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-hazard.markdown-source-view img[src$=\"#left\"],\n.json5e-hazard.markdown-source-view img[src$=\"#right\"],\n.json5e-item.markdown-preview-view div[src$=\"#left\"],\n.json5e-item.markdown-preview-view div[src$=\"#right\"],\n.json5e-item.markdown-preview-view span[src$=\"#left\"],\n.json5e-item.markdown-preview-view span[src$=\"#right\"],\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-item.markdown-preview-view img[src$=\"#left\"],\n.json5e-item.markdown-preview-view img[src$=\"#right\"],\n.json5e-item.markdown-source-view div[src$=\"#left\"],\n.json5e-item.markdown-source-view div[src$=\"#right\"],\n.json5e-item.markdown-source-view span[src$=\"#left\"],\n.json5e-item.markdown-source-view span[src$=\"#right\"],\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-item.markdown-source-view img[src$=\"#left\"],\n.json5e-item.markdown-source-view img[src$=\"#right\"],\n.json5e-monster.markdown-preview-view div[src$=\"#left\"],\n.json5e-monster.markdown-preview-view div[src$=\"#right\"],\n.json5e-monster.markdown-preview-view span[src$=\"#left\"],\n.json5e-monster.markdown-preview-view span[src$=\"#right\"],\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-monster.markdown-preview-view img[src$=\"#left\"],\n.json5e-monster.markdown-preview-view img[src$=\"#right\"],\n.json5e-monster.markdown-source-view div[src$=\"#left\"],\n.json5e-monster.markdown-source-view div[src$=\"#right\"],\n.json5e-monster.markdown-source-view span[src$=\"#left\"],\n.json5e-monster.markdown-source-view span[src$=\"#right\"],\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-monster.markdown-source-view img[src$=\"#left\"],\n.json5e-monster.markdown-source-view img[src$=\"#right\"],\n.json5e-note.markdown-preview-view div[src$=\"#left\"],\n.json5e-note.markdown-preview-view div[src$=\"#right\"],\n.json5e-note.markdown-preview-view span[src$=\"#left\"],\n.json5e-note.markdown-preview-view span[src$=\"#right\"],\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-note.markdown-preview-view img[src$=\"#left\"],\n.json5e-note.markdown-preview-view img[src$=\"#right\"],\n.json5e-note.markdown-source-view div[src$=\"#left\"],\n.json5e-note.markdown-source-view div[src$=\"#right\"],\n.json5e-note.markdown-source-view span[src$=\"#left\"],\n.json5e-note.markdown-source-view span[src$=\"#right\"],\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-note.markdown-source-view img[src$=\"#left\"],\n.json5e-note.markdown-source-view img[src$=\"#right\"],\n.json5e-object.markdown-preview-view div[src$=\"#left\"],\n.json5e-object.markdown-preview-view div[src$=\"#right\"],\n.json5e-object.markdown-preview-view span[src$=\"#left\"],\n.json5e-object.markdown-preview-view span[src$=\"#right\"],\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-object.markdown-preview-view img[src$=\"#left\"],\n.json5e-object.markdown-preview-view img[src$=\"#right\"],\n.json5e-object.markdown-source-view div[src$=\"#left\"],\n.json5e-object.markdown-source-view div[src$=\"#right\"],\n.json5e-object.markdown-source-view span[src$=\"#left\"],\n.json5e-object.markdown-source-view span[src$=\"#right\"],\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-object.markdown-source-view img[src$=\"#left\"],\n.json5e-object.markdown-source-view img[src$=\"#right\"],\n.json5e-psionic.markdown-preview-view div[src$=\"#left\"],\n.json5e-psionic.markdown-preview-view div[src$=\"#right\"],\n.json5e-psionic.markdown-preview-view span[src$=\"#left\"],\n.json5e-psionic.markdown-preview-view span[src$=\"#right\"],\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-psionic.markdown-preview-view img[src$=\"#left\"],\n.json5e-psionic.markdown-preview-view img[src$=\"#right\"],\n.json5e-psionic.markdown-source-view div[src$=\"#left\"],\n.json5e-psionic.markdown-source-view div[src$=\"#right\"],\n.json5e-psionic.markdown-source-view span[src$=\"#left\"],\n.json5e-psionic.markdown-source-view span[src$=\"#right\"],\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-psionic.markdown-source-view img[src$=\"#left\"],\n.json5e-psionic.markdown-source-view img[src$=\"#right\"],\n.json5e-race.markdown-preview-view div[src$=\"#left\"],\n.json5e-race.markdown-preview-view div[src$=\"#right\"],\n.json5e-race.markdown-preview-view span[src$=\"#left\"],\n.json5e-race.markdown-preview-view span[src$=\"#right\"],\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-race.markdown-preview-view img[src$=\"#left\"],\n.json5e-race.markdown-preview-view img[src$=\"#right\"],\n.json5e-race.markdown-source-view div[src$=\"#left\"],\n.json5e-race.markdown-source-view div[src$=\"#right\"],\n.json5e-race.markdown-source-view span[src$=\"#left\"],\n.json5e-race.markdown-source-view span[src$=\"#right\"],\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-race.markdown-source-view img[src$=\"#left\"],\n.json5e-race.markdown-source-view img[src$=\"#right\"],\n.json5e-reward.markdown-preview-view div[src$=\"#left\"],\n.json5e-reward.markdown-preview-view div[src$=\"#right\"],\n.json5e-reward.markdown-preview-view span[src$=\"#left\"],\n.json5e-reward.markdown-preview-view span[src$=\"#right\"],\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-reward.markdown-preview-view img[src$=\"#left\"],\n.json5e-reward.markdown-preview-view img[src$=\"#right\"],\n.json5e-reward.markdown-source-view div[src$=\"#left\"],\n.json5e-reward.markdown-source-view div[src$=\"#right\"],\n.json5e-reward.markdown-source-view span[src$=\"#left\"],\n.json5e-reward.markdown-source-view span[src$=\"#right\"],\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-reward.markdown-source-view img[src$=\"#left\"],\n.json5e-reward.markdown-source-view img[src$=\"#right\"],\n.json5e-species.markdown-preview-view div[src$=\"#left\"],\n.json5e-species.markdown-preview-view div[src$=\"#right\"],\n.json5e-species.markdown-preview-view span[src$=\"#left\"],\n.json5e-species.markdown-preview-view span[src$=\"#right\"],\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-species.markdown-preview-view img[src$=\"#left\"],\n.json5e-species.markdown-preview-view img[src$=\"#right\"],\n.json5e-species.markdown-source-view div[src$=\"#left\"],\n.json5e-species.markdown-source-view div[src$=\"#right\"],\n.json5e-species.markdown-source-view span[src$=\"#left\"],\n.json5e-species.markdown-source-view span[src$=\"#right\"],\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-species.markdown-source-view img[src$=\"#left\"],\n.json5e-species.markdown-source-view img[src$=\"#right\"],\n.json5e-spell.markdown-preview-view div[src$=\"#left\"],\n.json5e-spell.markdown-preview-view div[src$=\"#right\"],\n.json5e-spell.markdown-preview-view span[src$=\"#left\"],\n.json5e-spell.markdown-preview-view span[src$=\"#right\"],\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-spell.markdown-preview-view img[src$=\"#left\"],\n.json5e-spell.markdown-preview-view img[src$=\"#right\"],\n.json5e-spell.markdown-source-view div[src$=\"#left\"],\n.json5e-spell.markdown-source-view div[src$=\"#right\"],\n.json5e-spell.markdown-source-view span[src$=\"#left\"],\n.json5e-spell.markdown-source-view span[src$=\"#right\"],\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-spell.markdown-source-view img[src$=\"#left\"],\n.json5e-spell.markdown-source-view img[src$=\"#right\"],\n.json5e-vehicle.markdown-preview-view div[src$=\"#left\"],\n.json5e-vehicle.markdown-preview-view div[src$=\"#right\"],\n.json5e-vehicle.markdown-preview-view span[src$=\"#left\"],\n.json5e-vehicle.markdown-preview-view span[src$=\"#right\"],\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#left\"],\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#right\"],\n.json5e-vehicle.markdown-preview-view img[src$=\"#left\"],\n.json5e-vehicle.markdown-preview-view img[src$=\"#right\"],\n.json5e-vehicle.markdown-source-view div[src$=\"#left\"],\n.json5e-vehicle.markdown-source-view div[src$=\"#right\"],\n.json5e-vehicle.markdown-source-view span[src$=\"#left\"],\n.json5e-vehicle.markdown-source-view span[src$=\"#right\"],\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#left\"],\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#right\"],\n.json5e-vehicle.markdown-source-view img[src$=\"#left\"],\n.json5e-vehicle.markdown-source-view img[src$=\"#right\"] {\n  max-width: 50%;\n}\n.json5e-background.markdown-preview-view div[src$=\"#card\"], .json5e-background.markdown-preview-view div[src$=\"#token\"],\n.json5e-background.markdown-preview-view span[src$=\"#card\"],\n.json5e-background.markdown-preview-view span[src$=\"#token\"],\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-background.markdown-preview-view img[src$=\"#card\"],\n.json5e-background.markdown-preview-view img[src$=\"#token\"], .json5e-background.markdown-source-view div[src$=\"#card\"], .json5e-background.markdown-source-view div[src$=\"#token\"],\n.json5e-background.markdown-source-view span[src$=\"#card\"],\n.json5e-background.markdown-source-view span[src$=\"#token\"],\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-background.markdown-source-view img[src$=\"#card\"],\n.json5e-background.markdown-source-view img[src$=\"#token\"],\n.json5e-class.markdown-preview-view div[src$=\"#card\"],\n.json5e-class.markdown-preview-view div[src$=\"#token\"],\n.json5e-class.markdown-preview-view span[src$=\"#card\"],\n.json5e-class.markdown-preview-view span[src$=\"#token\"],\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-class.markdown-preview-view img[src$=\"#card\"],\n.json5e-class.markdown-preview-view img[src$=\"#token\"],\n.json5e-class.markdown-source-view div[src$=\"#card\"],\n.json5e-class.markdown-source-view div[src$=\"#token\"],\n.json5e-class.markdown-source-view span[src$=\"#card\"],\n.json5e-class.markdown-source-view span[src$=\"#token\"],\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-class.markdown-source-view img[src$=\"#card\"],\n.json5e-class.markdown-source-view img[src$=\"#token\"],\n.json5e-deck.markdown-preview-view div[src$=\"#card\"],\n.json5e-deck.markdown-preview-view div[src$=\"#token\"],\n.json5e-deck.markdown-preview-view span[src$=\"#card\"],\n.json5e-deck.markdown-preview-view span[src$=\"#token\"],\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-deck.markdown-preview-view img[src$=\"#card\"],\n.json5e-deck.markdown-preview-view img[src$=\"#token\"],\n.json5e-deck.markdown-source-view div[src$=\"#card\"],\n.json5e-deck.markdown-source-view div[src$=\"#token\"],\n.json5e-deck.markdown-source-view span[src$=\"#card\"],\n.json5e-deck.markdown-source-view span[src$=\"#token\"],\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-deck.markdown-source-view img[src$=\"#card\"],\n.json5e-deck.markdown-source-view img[src$=\"#token\"],\n.json5e-deity.markdown-preview-view div[src$=\"#card\"],\n.json5e-deity.markdown-preview-view div[src$=\"#token\"],\n.json5e-deity.markdown-preview-view span[src$=\"#card\"],\n.json5e-deity.markdown-preview-view span[src$=\"#token\"],\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-deity.markdown-preview-view img[src$=\"#card\"],\n.json5e-deity.markdown-preview-view img[src$=\"#token\"],\n.json5e-deity.markdown-source-view div[src$=\"#card\"],\n.json5e-deity.markdown-source-view div[src$=\"#token\"],\n.json5e-deity.markdown-source-view span[src$=\"#card\"],\n.json5e-deity.markdown-source-view span[src$=\"#token\"],\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-deity.markdown-source-view img[src$=\"#card\"],\n.json5e-deity.markdown-source-view img[src$=\"#token\"],\n.json5e-feat.markdown-preview-view div[src$=\"#card\"],\n.json5e-feat.markdown-preview-view div[src$=\"#token\"],\n.json5e-feat.markdown-preview-view span[src$=\"#card\"],\n.json5e-feat.markdown-preview-view span[src$=\"#token\"],\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-feat.markdown-preview-view img[src$=\"#card\"],\n.json5e-feat.markdown-preview-view img[src$=\"#token\"],\n.json5e-feat.markdown-source-view div[src$=\"#card\"],\n.json5e-feat.markdown-source-view div[src$=\"#token\"],\n.json5e-feat.markdown-source-view span[src$=\"#card\"],\n.json5e-feat.markdown-source-view span[src$=\"#token\"],\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-feat.markdown-source-view img[src$=\"#card\"],\n.json5e-feat.markdown-source-view img[src$=\"#token\"],\n.json5e-hazard.markdown-preview-view div[src$=\"#card\"],\n.json5e-hazard.markdown-preview-view div[src$=\"#token\"],\n.json5e-hazard.markdown-preview-view span[src$=\"#card\"],\n.json5e-hazard.markdown-preview-view span[src$=\"#token\"],\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-hazard.markdown-preview-view img[src$=\"#card\"],\n.json5e-hazard.markdown-preview-view img[src$=\"#token\"],\n.json5e-hazard.markdown-source-view div[src$=\"#card\"],\n.json5e-hazard.markdown-source-view div[src$=\"#token\"],\n.json5e-hazard.markdown-source-view span[src$=\"#card\"],\n.json5e-hazard.markdown-source-view span[src$=\"#token\"],\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-hazard.markdown-source-view img[src$=\"#card\"],\n.json5e-hazard.markdown-source-view img[src$=\"#token\"],\n.json5e-item.markdown-preview-view div[src$=\"#card\"],\n.json5e-item.markdown-preview-view div[src$=\"#token\"],\n.json5e-item.markdown-preview-view span[src$=\"#card\"],\n.json5e-item.markdown-preview-view span[src$=\"#token\"],\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-item.markdown-preview-view img[src$=\"#card\"],\n.json5e-item.markdown-preview-view img[src$=\"#token\"],\n.json5e-item.markdown-source-view div[src$=\"#card\"],\n.json5e-item.markdown-source-view div[src$=\"#token\"],\n.json5e-item.markdown-source-view span[src$=\"#card\"],\n.json5e-item.markdown-source-view span[src$=\"#token\"],\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-item.markdown-source-view img[src$=\"#card\"],\n.json5e-item.markdown-source-view img[src$=\"#token\"],\n.json5e-monster.markdown-preview-view div[src$=\"#card\"],\n.json5e-monster.markdown-preview-view div[src$=\"#token\"],\n.json5e-monster.markdown-preview-view span[src$=\"#card\"],\n.json5e-monster.markdown-preview-view span[src$=\"#token\"],\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-monster.markdown-preview-view img[src$=\"#card\"],\n.json5e-monster.markdown-preview-view img[src$=\"#token\"],\n.json5e-monster.markdown-source-view div[src$=\"#card\"],\n.json5e-monster.markdown-source-view div[src$=\"#token\"],\n.json5e-monster.markdown-source-view span[src$=\"#card\"],\n.json5e-monster.markdown-source-view span[src$=\"#token\"],\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-monster.markdown-source-view img[src$=\"#card\"],\n.json5e-monster.markdown-source-view img[src$=\"#token\"],\n.json5e-note.markdown-preview-view div[src$=\"#card\"],\n.json5e-note.markdown-preview-view div[src$=\"#token\"],\n.json5e-note.markdown-preview-view span[src$=\"#card\"],\n.json5e-note.markdown-preview-view span[src$=\"#token\"],\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-note.markdown-preview-view img[src$=\"#card\"],\n.json5e-note.markdown-preview-view img[src$=\"#token\"],\n.json5e-note.markdown-source-view div[src$=\"#card\"],\n.json5e-note.markdown-source-view div[src$=\"#token\"],\n.json5e-note.markdown-source-view span[src$=\"#card\"],\n.json5e-note.markdown-source-view span[src$=\"#token\"],\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-note.markdown-source-view img[src$=\"#card\"],\n.json5e-note.markdown-source-view img[src$=\"#token\"],\n.json5e-object.markdown-preview-view div[src$=\"#card\"],\n.json5e-object.markdown-preview-view div[src$=\"#token\"],\n.json5e-object.markdown-preview-view span[src$=\"#card\"],\n.json5e-object.markdown-preview-view span[src$=\"#token\"],\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-object.markdown-preview-view img[src$=\"#card\"],\n.json5e-object.markdown-preview-view img[src$=\"#token\"],\n.json5e-object.markdown-source-view div[src$=\"#card\"],\n.json5e-object.markdown-source-view div[src$=\"#token\"],\n.json5e-object.markdown-source-view span[src$=\"#card\"],\n.json5e-object.markdown-source-view span[src$=\"#token\"],\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-object.markdown-source-view img[src$=\"#card\"],\n.json5e-object.markdown-source-view img[src$=\"#token\"],\n.json5e-psionic.markdown-preview-view div[src$=\"#card\"],\n.json5e-psionic.markdown-preview-view div[src$=\"#token\"],\n.json5e-psionic.markdown-preview-view span[src$=\"#card\"],\n.json5e-psionic.markdown-preview-view span[src$=\"#token\"],\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-psionic.markdown-preview-view img[src$=\"#card\"],\n.json5e-psionic.markdown-preview-view img[src$=\"#token\"],\n.json5e-psionic.markdown-source-view div[src$=\"#card\"],\n.json5e-psionic.markdown-source-view div[src$=\"#token\"],\n.json5e-psionic.markdown-source-view span[src$=\"#card\"],\n.json5e-psionic.markdown-source-view span[src$=\"#token\"],\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-psionic.markdown-source-view img[src$=\"#card\"],\n.json5e-psionic.markdown-source-view img[src$=\"#token\"],\n.json5e-race.markdown-preview-view div[src$=\"#card\"],\n.json5e-race.markdown-preview-view div[src$=\"#token\"],\n.json5e-race.markdown-preview-view span[src$=\"#card\"],\n.json5e-race.markdown-preview-view span[src$=\"#token\"],\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-race.markdown-preview-view img[src$=\"#card\"],\n.json5e-race.markdown-preview-view img[src$=\"#token\"],\n.json5e-race.markdown-source-view div[src$=\"#card\"],\n.json5e-race.markdown-source-view div[src$=\"#token\"],\n.json5e-race.markdown-source-view span[src$=\"#card\"],\n.json5e-race.markdown-source-view span[src$=\"#token\"],\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-race.markdown-source-view img[src$=\"#card\"],\n.json5e-race.markdown-source-view img[src$=\"#token\"],\n.json5e-reward.markdown-preview-view div[src$=\"#card\"],\n.json5e-reward.markdown-preview-view div[src$=\"#token\"],\n.json5e-reward.markdown-preview-view span[src$=\"#card\"],\n.json5e-reward.markdown-preview-view span[src$=\"#token\"],\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-reward.markdown-preview-view img[src$=\"#card\"],\n.json5e-reward.markdown-preview-view img[src$=\"#token\"],\n.json5e-reward.markdown-source-view div[src$=\"#card\"],\n.json5e-reward.markdown-source-view div[src$=\"#token\"],\n.json5e-reward.markdown-source-view span[src$=\"#card\"],\n.json5e-reward.markdown-source-view span[src$=\"#token\"],\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-reward.markdown-source-view img[src$=\"#card\"],\n.json5e-reward.markdown-source-view img[src$=\"#token\"],\n.json5e-species.markdown-preview-view div[src$=\"#card\"],\n.json5e-species.markdown-preview-view div[src$=\"#token\"],\n.json5e-species.markdown-preview-view span[src$=\"#card\"],\n.json5e-species.markdown-preview-view span[src$=\"#token\"],\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-species.markdown-preview-view img[src$=\"#card\"],\n.json5e-species.markdown-preview-view img[src$=\"#token\"],\n.json5e-species.markdown-source-view div[src$=\"#card\"],\n.json5e-species.markdown-source-view div[src$=\"#token\"],\n.json5e-species.markdown-source-view span[src$=\"#card\"],\n.json5e-species.markdown-source-view span[src$=\"#token\"],\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-species.markdown-source-view img[src$=\"#card\"],\n.json5e-species.markdown-source-view img[src$=\"#token\"],\n.json5e-spell.markdown-preview-view div[src$=\"#card\"],\n.json5e-spell.markdown-preview-view div[src$=\"#token\"],\n.json5e-spell.markdown-preview-view span[src$=\"#card\"],\n.json5e-spell.markdown-preview-view span[src$=\"#token\"],\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-spell.markdown-preview-view img[src$=\"#card\"],\n.json5e-spell.markdown-preview-view img[src$=\"#token\"],\n.json5e-spell.markdown-source-view div[src$=\"#card\"],\n.json5e-spell.markdown-source-view div[src$=\"#token\"],\n.json5e-spell.markdown-source-view span[src$=\"#card\"],\n.json5e-spell.markdown-source-view span[src$=\"#token\"],\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-spell.markdown-source-view img[src$=\"#card\"],\n.json5e-spell.markdown-source-view img[src$=\"#token\"],\n.json5e-vehicle.markdown-preview-view div[src$=\"#card\"],\n.json5e-vehicle.markdown-preview-view div[src$=\"#token\"],\n.json5e-vehicle.markdown-preview-view span[src$=\"#card\"],\n.json5e-vehicle.markdown-preview-view span[src$=\"#token\"],\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#card\"],\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#token\"],\n.json5e-vehicle.markdown-preview-view img[src$=\"#card\"],\n.json5e-vehicle.markdown-preview-view img[src$=\"#token\"],\n.json5e-vehicle.markdown-source-view div[src$=\"#card\"],\n.json5e-vehicle.markdown-source-view div[src$=\"#token\"],\n.json5e-vehicle.markdown-source-view span[src$=\"#card\"],\n.json5e-vehicle.markdown-source-view span[src$=\"#token\"],\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#card\"],\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#token\"],\n.json5e-vehicle.markdown-source-view img[src$=\"#card\"],\n.json5e-vehicle.markdown-source-view img[src$=\"#token\"] {\n  width: 150px;\n}\n.json5e-background.markdown-preview-view div[src$=\"#symbol\"], .json5e-background.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-background.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-background.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-background.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-background.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-background.markdown-preview-view img[src$=\"#portrait\"], .json5e-background.markdown-source-view div[src$=\"#symbol\"], .json5e-background.markdown-source-view div[src$=\"#portrait\"],\n.json5e-background.markdown-source-view span[src$=\"#symbol\"],\n.json5e-background.markdown-source-view span[src$=\"#portrait\"],\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-background.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-background.markdown-source-view img[src$=\"#symbol\"],\n.json5e-background.markdown-source-view img[src$=\"#portrait\"],\n.json5e-class.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-class.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-class.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-class.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-class.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-class.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-class.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-class.markdown-source-view div[src$=\"#symbol\"],\n.json5e-class.markdown-source-view div[src$=\"#portrait\"],\n.json5e-class.markdown-source-view span[src$=\"#symbol\"],\n.json5e-class.markdown-source-view span[src$=\"#portrait\"],\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-class.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-class.markdown-source-view img[src$=\"#symbol\"],\n.json5e-class.markdown-source-view img[src$=\"#portrait\"],\n.json5e-deck.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-deck.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-deck.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-deck.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-deck.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-deck.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-deck.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-deck.markdown-source-view div[src$=\"#symbol\"],\n.json5e-deck.markdown-source-view div[src$=\"#portrait\"],\n.json5e-deck.markdown-source-view span[src$=\"#symbol\"],\n.json5e-deck.markdown-source-view span[src$=\"#portrait\"],\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-deck.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-deck.markdown-source-view img[src$=\"#symbol\"],\n.json5e-deck.markdown-source-view img[src$=\"#portrait\"],\n.json5e-deity.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-deity.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-deity.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-deity.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-deity.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-deity.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-deity.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-deity.markdown-source-view div[src$=\"#symbol\"],\n.json5e-deity.markdown-source-view div[src$=\"#portrait\"],\n.json5e-deity.markdown-source-view span[src$=\"#symbol\"],\n.json5e-deity.markdown-source-view span[src$=\"#portrait\"],\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-deity.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-deity.markdown-source-view img[src$=\"#symbol\"],\n.json5e-deity.markdown-source-view img[src$=\"#portrait\"],\n.json5e-feat.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-feat.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-feat.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-feat.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-feat.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-feat.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-feat.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-feat.markdown-source-view div[src$=\"#symbol\"],\n.json5e-feat.markdown-source-view div[src$=\"#portrait\"],\n.json5e-feat.markdown-source-view span[src$=\"#symbol\"],\n.json5e-feat.markdown-source-view span[src$=\"#portrait\"],\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-feat.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-feat.markdown-source-view img[src$=\"#symbol\"],\n.json5e-feat.markdown-source-view img[src$=\"#portrait\"],\n.json5e-hazard.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-hazard.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-hazard.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-hazard.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-hazard.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-hazard.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-hazard.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-hazard.markdown-source-view div[src$=\"#symbol\"],\n.json5e-hazard.markdown-source-view div[src$=\"#portrait\"],\n.json5e-hazard.markdown-source-view span[src$=\"#symbol\"],\n.json5e-hazard.markdown-source-view span[src$=\"#portrait\"],\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-hazard.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-hazard.markdown-source-view img[src$=\"#symbol\"],\n.json5e-hazard.markdown-source-view img[src$=\"#portrait\"],\n.json5e-item.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-item.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-item.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-item.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-item.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-item.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-item.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-item.markdown-source-view div[src$=\"#symbol\"],\n.json5e-item.markdown-source-view div[src$=\"#portrait\"],\n.json5e-item.markdown-source-view span[src$=\"#symbol\"],\n.json5e-item.markdown-source-view span[src$=\"#portrait\"],\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-item.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-item.markdown-source-view img[src$=\"#symbol\"],\n.json5e-item.markdown-source-view img[src$=\"#portrait\"],\n.json5e-monster.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-monster.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-monster.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-monster.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-monster.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-monster.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-monster.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-monster.markdown-source-view div[src$=\"#symbol\"],\n.json5e-monster.markdown-source-view div[src$=\"#portrait\"],\n.json5e-monster.markdown-source-view span[src$=\"#symbol\"],\n.json5e-monster.markdown-source-view span[src$=\"#portrait\"],\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-monster.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-monster.markdown-source-view img[src$=\"#symbol\"],\n.json5e-monster.markdown-source-view img[src$=\"#portrait\"],\n.json5e-note.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-note.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-note.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-note.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-note.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-note.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-note.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-note.markdown-source-view div[src$=\"#symbol\"],\n.json5e-note.markdown-source-view div[src$=\"#portrait\"],\n.json5e-note.markdown-source-view span[src$=\"#symbol\"],\n.json5e-note.markdown-source-view span[src$=\"#portrait\"],\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-note.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-note.markdown-source-view img[src$=\"#symbol\"],\n.json5e-note.markdown-source-view img[src$=\"#portrait\"],\n.json5e-object.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-object.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-object.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-object.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-object.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-object.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-object.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-object.markdown-source-view div[src$=\"#symbol\"],\n.json5e-object.markdown-source-view div[src$=\"#portrait\"],\n.json5e-object.markdown-source-view span[src$=\"#symbol\"],\n.json5e-object.markdown-source-view span[src$=\"#portrait\"],\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-object.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-object.markdown-source-view img[src$=\"#symbol\"],\n.json5e-object.markdown-source-view img[src$=\"#portrait\"],\n.json5e-psionic.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-psionic.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-psionic.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-psionic.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-psionic.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-psionic.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-psionic.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-psionic.markdown-source-view div[src$=\"#symbol\"],\n.json5e-psionic.markdown-source-view div[src$=\"#portrait\"],\n.json5e-psionic.markdown-source-view span[src$=\"#symbol\"],\n.json5e-psionic.markdown-source-view span[src$=\"#portrait\"],\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-psionic.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-psionic.markdown-source-view img[src$=\"#symbol\"],\n.json5e-psionic.markdown-source-view img[src$=\"#portrait\"],\n.json5e-race.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-race.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-race.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-race.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-race.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-race.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-race.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-race.markdown-source-view div[src$=\"#symbol\"],\n.json5e-race.markdown-source-view div[src$=\"#portrait\"],\n.json5e-race.markdown-source-view span[src$=\"#symbol\"],\n.json5e-race.markdown-source-view span[src$=\"#portrait\"],\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-race.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-race.markdown-source-view img[src$=\"#symbol\"],\n.json5e-race.markdown-source-view img[src$=\"#portrait\"],\n.json5e-reward.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-reward.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-reward.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-reward.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-reward.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-reward.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-reward.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-reward.markdown-source-view div[src$=\"#symbol\"],\n.json5e-reward.markdown-source-view div[src$=\"#portrait\"],\n.json5e-reward.markdown-source-view span[src$=\"#symbol\"],\n.json5e-reward.markdown-source-view span[src$=\"#portrait\"],\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-reward.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-reward.markdown-source-view img[src$=\"#symbol\"],\n.json5e-reward.markdown-source-view img[src$=\"#portrait\"],\n.json5e-species.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-species.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-species.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-species.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-species.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-species.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-species.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-species.markdown-source-view div[src$=\"#symbol\"],\n.json5e-species.markdown-source-view div[src$=\"#portrait\"],\n.json5e-species.markdown-source-view span[src$=\"#symbol\"],\n.json5e-species.markdown-source-view span[src$=\"#portrait\"],\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-species.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-species.markdown-source-view img[src$=\"#symbol\"],\n.json5e-species.markdown-source-view img[src$=\"#portrait\"],\n.json5e-spell.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-spell.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-spell.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-spell.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-spell.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-spell.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-spell.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-spell.markdown-source-view div[src$=\"#symbol\"],\n.json5e-spell.markdown-source-view div[src$=\"#portrait\"],\n.json5e-spell.markdown-source-view span[src$=\"#symbol\"],\n.json5e-spell.markdown-source-view span[src$=\"#portrait\"],\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-spell.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-spell.markdown-source-view img[src$=\"#symbol\"],\n.json5e-spell.markdown-source-view img[src$=\"#portrait\"],\n.json5e-vehicle.markdown-preview-view div[src$=\"#symbol\"],\n.json5e-vehicle.markdown-preview-view div[src$=\"#portrait\"],\n.json5e-vehicle.markdown-preview-view span[src$=\"#symbol\"],\n.json5e-vehicle.markdown-preview-view span[src$=\"#portrait\"],\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#symbol\"],\n.json5e-vehicle.markdown-preview-view span.internal-embed[src$=\"#portrait\"],\n.json5e-vehicle.markdown-preview-view img[src$=\"#symbol\"],\n.json5e-vehicle.markdown-preview-view img[src$=\"#portrait\"],\n.json5e-vehicle.markdown-source-view div[src$=\"#symbol\"],\n.json5e-vehicle.markdown-source-view div[src$=\"#portrait\"],\n.json5e-vehicle.markdown-source-view span[src$=\"#symbol\"],\n.json5e-vehicle.markdown-source-view span[src$=\"#portrait\"],\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#symbol\"],\n.json5e-vehicle.markdown-source-view span.internal-embed[src$=\"#portrait\"],\n.json5e-vehicle.markdown-source-view img[src$=\"#symbol\"],\n.json5e-vehicle.markdown-source-view img[src$=\"#portrait\"] {\n  width: 200px;\n}\n\n/* For decks of cards, ensure images don't overlap */\n.json5e-deck h3 {\n  clear: both;\n}\n"
  },
  {
    "path": "examples/css-snippets/dnd5e-only-admonitions.css",
    "content": "@charset \"UTF-8\";\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/dnd5e-only-admonitions.scss */\nbody {\n  --admonition-charm: 211,141,159;\n  --admonition-charm-text: var(--admonition-charm);\n  --admonition-letter: 98, 159, 197;\n  --admonition-npc: 102, 121, 137;\n  --admonition-scene: 139, 167, 145;\n  --admonition-skill: 236,201,134;\n  --admonition-skill-text: var(--admonition-skill);\n  --admonition-weather: 53,119,174;\n  --admonition-flowchart: 72,72,72;\n}\n\n.theme-light {\n  --admonition-charm: 222,170,184;\n  --admonition-charm-text: 167,92,112;\n  --admonition-npc: 58, 125, 127;\n  --admonition-scene: 92, 122, 99;\n  --admonition-skill: 221,178,84;\n  --admonition-skill-text: 157,101,83;\n}\n\n.callout[data-callout=charm] {\n  --callout-color: var(--admonition-charm);\n  --callout-title-color: rgb(var(--admonition-charm-text));\n}\n.callout[data-callout=charm] .callout-title {\n  color: var(--callout-title-color);\n}\n\n.callout[data-callout=letter] {\n  --callout-color: var(--admonition-letter);\n}\n\n.callout[data-callout=npc] {\n  --callout-color: var(--admonition-npc);\n}\n\n.callout[data-callout=readaloud],\n.callout[data-callout=scene] {\n  --callout-color: var(--admonition-scene);\n}\n\n.callout[data-callout=skill] {\n  --callout-color: var(--admonition-skill);\n  --callout-title-color: rgb(var(--admonition-skill-text));\n}\n\n.callout[data-callout=weather] {\n  --callout-color: var(--admonition-weather);\n}\n\n.callout[data-callout=flowchart] {\n  --callout-color: var(--admonition-flowchart);\n  --callout-border-width: 0.10rem;\n}\n\n.callout[data-callout^=embed-] {\n  --embed-border-left: none;\n  --embed-border-right: none;\n  --embed-padding: 0;\n}\n\n.json5e-background div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]),\n.json5e-class div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]),\n.json5e-deck div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]),\n.json5e-deity div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]),\n.json5e-feat div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]),\n.json5e-hazard div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]),\n.json5e-item div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]),\n.json5e-monster div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]),\n.json5e-note div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]),\n.json5e-object div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]),\n.json5e-psionic div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]),\n.json5e-race div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]),\n.json5e-reward div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]),\n.json5e-spell div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]),\n.json5e-vehicle div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]) {\n  position: relative;\n}\n.json5e-background div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]):after,\n.json5e-class div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]):after,\n.json5e-deck div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]):after,\n.json5e-deity div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]):after,\n.json5e-feat div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]):after,\n.json5e-hazard div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]):after,\n.json5e-item div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]):after,\n.json5e-monster div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]):after,\n.json5e-note div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]):after,\n.json5e-object div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]):after,\n.json5e-psionic div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]):after,\n.json5e-race div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]):after,\n.json5e-reward div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]):after,\n.json5e-spell div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]):after,\n.json5e-vehicle div:has(> .callout[data-callout=flowchart]):has(+ div > .callout[data-callout=flowchart]):after {\n  content: \"↓\";\n  color: var(--admonition-flowchart);\n  display: block;\n  position: absolute;\n  bottom: -10px;\n  left: 50%;\n  margin-left: 7px;\n  width: 14px;\n  height: 14px;\n  font-size: 14px;\n  text-align: center;\n}\n.json5e-background .callout[data-callout=gallery],\n.json5e-class .callout[data-callout=gallery],\n.json5e-deck .callout[data-callout=gallery],\n.json5e-deity .callout[data-callout=gallery],\n.json5e-feat .callout[data-callout=gallery],\n.json5e-hazard .callout[data-callout=gallery],\n.json5e-item .callout[data-callout=gallery],\n.json5e-monster .callout[data-callout=gallery],\n.json5e-note .callout[data-callout=gallery],\n.json5e-object .callout[data-callout=gallery],\n.json5e-psionic .callout[data-callout=gallery],\n.json5e-race .callout[data-callout=gallery],\n.json5e-reward .callout[data-callout=gallery],\n.json5e-spell .callout[data-callout=gallery],\n.json5e-vehicle .callout[data-callout=gallery] {\n  --callout-color: transparent;\n  --callout-border-width: 0;\n}\n.json5e-background .callout[data-callout=gallery] .callout-content p,\n.json5e-class .callout[data-callout=gallery] .callout-content p,\n.json5e-deck .callout[data-callout=gallery] .callout-content p,\n.json5e-deity .callout[data-callout=gallery] .callout-content p,\n.json5e-feat .callout[data-callout=gallery] .callout-content p,\n.json5e-hazard .callout[data-callout=gallery] .callout-content p,\n.json5e-item .callout[data-callout=gallery] .callout-content p,\n.json5e-monster .callout[data-callout=gallery] .callout-content p,\n.json5e-note .callout[data-callout=gallery] .callout-content p,\n.json5e-object .callout[data-callout=gallery] .callout-content p,\n.json5e-psionic .callout[data-callout=gallery] .callout-content p,\n.json5e-race .callout[data-callout=gallery] .callout-content p,\n.json5e-reward .callout[data-callout=gallery] .callout-content p,\n.json5e-spell .callout[data-callout=gallery] .callout-content p,\n.json5e-vehicle .callout[data-callout=gallery] .callout-content p {\n  display: flex;\n  flex-wrap: wrap;\n  justify-content: space-evenly;\n  align-content: center;\n}\n.json5e-background .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"],\n.json5e-background .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"],\n.json5e-class .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"],\n.json5e-class .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"],\n.json5e-deck .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"],\n.json5e-deck .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"],\n.json5e-deity .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"],\n.json5e-deity .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"],\n.json5e-feat .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"],\n.json5e-feat .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"],\n.json5e-hazard .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"],\n.json5e-hazard .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"],\n.json5e-item .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"],\n.json5e-item .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"],\n.json5e-monster .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"],\n.json5e-monster .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"],\n.json5e-note .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"],\n.json5e-note .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"],\n.json5e-object .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"],\n.json5e-object .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"],\n.json5e-psionic .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"],\n.json5e-psionic .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"],\n.json5e-race .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"],\n.json5e-race .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"],\n.json5e-reward .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"],\n.json5e-reward .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"],\n.json5e-spell .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"],\n.json5e-spell .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"],\n.json5e-vehicle .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"],\n.json5e-vehicle .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] {\n  max-width: 49%;\n}\n.json5e-background .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"] img,\n.json5e-background .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] img,\n.json5e-class .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"] img,\n.json5e-class .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] img,\n.json5e-deck .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"] img,\n.json5e-deck .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] img,\n.json5e-deity .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"] img,\n.json5e-deity .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] img,\n.json5e-feat .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"] img,\n.json5e-feat .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] img,\n.json5e-hazard .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"] img,\n.json5e-hazard .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] img,\n.json5e-item .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"] img,\n.json5e-item .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] img,\n.json5e-monster .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"] img,\n.json5e-monster .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] img,\n.json5e-note .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"] img,\n.json5e-note .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] img,\n.json5e-object .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"] img,\n.json5e-object .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] img,\n.json5e-psionic .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"] img,\n.json5e-psionic .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] img,\n.json5e-race .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"] img,\n.json5e-race .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] img,\n.json5e-reward .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"] img,\n.json5e-reward .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] img,\n.json5e-spell .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"] img,\n.json5e-spell .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] img,\n.json5e-vehicle .callout[data-callout=gallery] .callout-content span[src$=\"#gallery\"] img,\n.json5e-vehicle .callout[data-callout=gallery] .callout-content div[src$=\"#gallery\"] img {\n  max-height: 40vh;\n}\n.json5e-background .callout[data-callout=gallery] .callout-title,\n.json5e-class .callout[data-callout=gallery] .callout-title,\n.json5e-deck .callout[data-callout=gallery] .callout-title,\n.json5e-deity .callout[data-callout=gallery] .callout-title,\n.json5e-feat .callout[data-callout=gallery] .callout-title,\n.json5e-hazard .callout[data-callout=gallery] .callout-title,\n.json5e-item .callout[data-callout=gallery] .callout-title,\n.json5e-monster .callout[data-callout=gallery] .callout-title,\n.json5e-note .callout[data-callout=gallery] .callout-title,\n.json5e-object .callout[data-callout=gallery] .callout-title,\n.json5e-psionic .callout[data-callout=gallery] .callout-title,\n.json5e-race .callout[data-callout=gallery] .callout-title,\n.json5e-reward .callout[data-callout=gallery] .callout-title,\n.json5e-spell .callout[data-callout=gallery] .callout-title,\n.json5e-vehicle .callout[data-callout=gallery] .callout-title {\n  display: none;\n}\n"
  },
  {
    "path": "examples/css-snippets/dnd5e-only-statblock.css",
    "content": "/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/dnd5e-only-statblock.scss */\nbody {\n  --statblock-accent: 201,60,60;\n}\n\n.admonition-statblock-parent {\n  clear: both;\n}\n\n.callout[data-callout=statblock] {\n  --callout-color: var(--statblock-accent);\n  --embed-border-left: none;\n  --embed-border-right: none;\n  --embed-padding: 0;\n}\n.callout[data-callout=statblock] .callout-title {\n  line-height: var(--line-height);\n}\n.callout[data-callout=statblock] .callout-title .callout-title-content {\n  flex: 2;\n  font-size: var(--h3-size);\n}\n.callout[data-callout=statblock] .callout-content > :first-child {\n  margin-top: 0.5em;\n}\n.callout[data-callout=statblock] .callout-content > :last-child {\n  margin-bottom: 0.5em;\n}\n.callout[data-callout=statblock] .callout-content h1,\n.callout[data-callout=statblock] .callout-content h2,\n.callout[data-callout=statblock] .callout-content h3 {\n  font-family: var(--default-font);\n  font-variant: common-ligatures small-caps;\n}\n.callout[data-callout=statblock] .callout-content h1 {\n  font-size: 1.4em;\n  line-height: 1.4em;\n  margin: 0;\n  padding: 0;\n}\n.callout[data-callout=statblock] .callout-content h2,\n.callout[data-callout=statblock] .callout-content h3 {\n  font-size: 1.2em;\n  line-height: 1.2em;\n  padding: 0.5em 0 0 0;\n  margin-top: 0.2em;\n  margin-bottom: 0.3em;\n  border-bottom: 1px solid rgb(var(--statblock-accent));\n}\n.callout[data-callout=statblock] .callout-content p {\n  line-height: 1.2em;\n  margin-block-start: 0.5em;\n}\n.callout[data-callout=statblock] .callout-content li {\n  line-height: 1.2em;\n  margin-block-start: 0.5em;\n}\n.callout[data-callout=statblock] .markdown-embed .markdown-embed-title,\n.callout[data-callout=statblock] .markdown-embed .mod-header {\n  display: none;\n}\n.callout[data-callout=statblock] .markdown-embed pre.frontmatter,\n.callout[data-callout=statblock] .markdown-embed h1[data-heading] {\n  display: none;\n}\n.callout[data-callout=statblock] .markdown-embed .markdown-embed-content {\n  max-height: unset;\n  overflow: unset;\n}\n.callout[data-callout=statblock] .markdown-embed .markdown-embed-content > .markdown-preview-view {\n  overflow-y: unset;\n}\n\n.callout[data-callout=statblock] div[src$=\"#token\"],\n.callout[data-callout=statblock] img[src$=\"#token\"] {\n  float: right;\n  padding-left: 5px;\n  width: 150px;\n}\n"
  },
  {
    "path": "examples/css-snippets/hide-markdown-link-url.css",
    "content": "@charset \"UTF-8\";\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/hide-markdown-link-url.scss */\n/* Collapse external links / markdown links in edit mode */\ndiv.cm-line:not(.cm-active) > .cm-string.cm-url:not(.cm-formatting) {\n  font-size: 0;\n}\ndiv.cm-line:not(.cm-active) > .cm-string.cm-url:not(.cm-formatting)::after {\n  content: \"➹\";\n  font-size: 0.8rem;\n}\n"
  },
  {
    "path": "examples/css-snippets/pf2-compendium.css",
    "content": "@charset \"UTF-8\";\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n@import url(\"https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap\");\n@import \"https://fonts.googleapis.com/css2?family=Oswald:wght@700&display=swap\";\n@font-face {\n  font-family: \"Pathfinder\";\n  src: url(\"data:application/x-font-ttf;charset=utf-8;base64,AAEAAAANAIAAAwBQRkZUTaBf/YkAAAkkAAAAHEdERUYAJQAAAAAJDAAAABhPUy8yDzE5wAAAAVgAAABgY21hcIUPLFwAAAHcAAABcGdhc3AAAAAQAAAJBAAAAAhnbHlmv0sljgAAA2AAAAIwaGVhZCgbkG4AAADcAAAANmhoZWEMNQhqAAABFAAAACRobXR4IOUAhwAAAbgAAAAkbG9jYQGoAlQAAANMAAAAFG1heHAADgA/AAABOAAAACBuYW1l5gKtSAAABZAAAAMGcG9zdJP9aoAAAAiYAAAAbAABAAAAARmaJrDlgl8PPPUACwQAAAAAAN/r3gMAAAAA4J1UVAAA/8AIcwPAAAAACAACAAAAAAAAAAEAAAPA/8AAAAigAAAAAAhzAAEAAAAAAAAAAAAAAAAAAAAJAAEAAAAJAD0ABAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAwVCAZAABQAAApkCzAAAAI8CmQLMAAAB6wAzAQkAAAAAAAAAAAAAAAAAAAABAAAAIAAAAAAAAAAAAAAAAABAAAArUwPA/8AAQAPAAEAAAAABAAAAAAAAAAAAAAAgAAEEAAAAAAAAAAFVAAAAAAAABFAAKAaAACAEAAACCKAALQQgABAAAAADAAAAAwAAABwAAQAAAAAAagADAAEAAAAcAAQATgAAAAwACAACAAQAASsyKz0rU//9//8AAAAAKzIrOitT//3//wAA1NIAANS1AAAAAQAMAAAADAAAABAAAAABAAMABQAGAAAABwAAAAABBgAAAQMAAAAAAAABAgAAAAIAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAIABAAeACgALwA8AEYAAEAAAAAAAAAAAACAAA5AgABAAAAAAAAAAAAAgAAOQIAAgAoACIEMwNlADcAPAAAASYnJicmJyYHBgcGBwYHBgc2NzY3Njc2NzYXFhcWFxYXFgcGBwYHBgcGBwYnFhcWNzY3Njc2NzYBBSc3AQQdFzs8VFNlZWpLQUEzMyQkERUdHSMkKSouVVFRQ0QwMBISEBEsK0RDVRwcGxs/REVFalRUNjYVFPyNAeltOP5MAkdHOTklJQ0ODwsYGCIiKyovGhYXEhINDQcMCwsdHi4uOTo3Ny0uISAMBAECAQ4DAwoPKSg5OEVE/opnldf++wAAAAMAIP/BBmADvwAFAAkADwAACQEHCQEXAQcXNyUBBxcHFwQa/gH2AQj++Pb+vLe3twTS/kDZ6OjZAcAB//f++P749wK2t7e3CAHA2Ojo2QAAAgAC/8AD/gPAAAUACQAACQEHCQEXAQcXNwP+/gD3AQn+9/f+u7e3twHAAgD3/vf+9/cCt7e3twAEAC3/wAhzA8AABQALAA8AFQAACQEHCQEXCQEHFwcXAQcXNyUBBxcHFwQp/gD3AQn+9/cGSv6Yr7u7r/nZt7e3BNz+P9rp6doBwAIA9/73/vf3AgoBaK66u64CFre3twgBwdno6dkAAAMAEP/ABBADwAAEAAkADwAACQU3FwcnASc3JzcBAhT9/AH9AgP+BP64bW1tbQE3cfv1fAFnA8D9+v4GAgMB/f4QbW1tbf6Ecfv2fP6ZAAAAAAASAN4AAQAAAAAAAQAKABYAAQAAAAAAAgAHADEAAQAAAAAAAwAKAE8AAQAAAAAABAAKAHAAAQAAAAAABQALAJMAAQAAAAAABgAKALUAAQAAAAAACgA2AS4AAQAAAAAADQAXAZUAAQAAAAAADgAoAf8AAwABBAkAAQAUAAAAAwABBAkAAgAOACEAAwABBAkAAwAUADkAAwABBAkABAAUAFoAAwABBAkABQAWAHsAAwABBAkABgAUAJ8AAwABBAkACgBsAMAAAwABBAkADQAuAWUAAwABBAkADgBQAa0AUABhAHQAaABmAGkAbgBkAGUAcgAAUGF0aGZpbmRlcgAAUgBlAGcAdQBsAGEAcgAAUmVndWxhcgAAUABhAHQAaABmAGkAbgBkAGUAcgAAUGF0aGZpbmRlcgAAUABhAHQAaABmAGkAbgBkAGUAcgAAUGF0aGZpbmRlcgAAVgBlAHIAcwBpAG8AbgAgADEALgAxAABWZXJzaW9uIDEuMQAAUABhAHQAaABmAGkAbgBkAGUAcgAAUGF0aGZpbmRlcgAAUABhAHQAaABmAGkAbgBkAGUAcgAgADIAZQAgAEEAYwB0AGkAbwBuACAARwBsAHkAcABoAHMACgBGAG8AbgB0ACAAZwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABJAGMAbwBNAG8AbwBuAC4AAFBhdGhmaW5kZXIgMmUgQWN0aW9uIEdseXBocwpGb250IGdlbmVyYXRlZCBieSBJY29Nb29uLgAAUABhAGkAegBvACAAQwBvAG0AbQB1AG4AaQB0AHkAIABMAGkAYwBlAG4AcwBlAABQYWl6byBDb21tdW5pdHkgTGljZW5zZQAAaAB0AHQAcABzADoALwAvAHAAYQBpAHoAbwAuAGMAbwBtAC8AYwBvAG0AbQB1AG4AaQB0AHkALwBjAG8AbQBtAHUAbgBpAHQAeQB1AHMAZQAAaHR0cHM6Ly9wYWl6by5jb20vY29tbXVuaXR5L2NvbW11bml0eXVzZQAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkAAAECAAIBAwEEAQUBBgEHAQgHdW5pMDAwMAd1bmkwMDAxB3VuaTJCMzIHdW5pMkIzQQd1bmkyQjNCB3VuaTJCM0QHdW5pMkI1MwABAAH//wAPAAEAAAAMAAAAEAAAAAIAAAAEAAAAAgAAAAAAAQAAAADf1ssxAAAAAN/r3gMAAAAA4J1UVA==\") format(\"truetype\");\n  font-weight: normal;\n  font-style: normal;\n  font-display: block;\n  unicode-range: U+2B32, U+2B3A, U+2B3B, U+2B3D, U+2B53;\n}\nbody {\n  --pf2e-font-monospace-default: Menlo, SFMono-Regular, Consolas, \"Roboto Mono\", \"Source Code Pro\", monospace;\n  --pf2e-font-monospace-theme: \"Fira Code\", Menlo, Monaco, \"Courier New\", \"Source Code Pro\", Jetbrains Mono;\n  --pf2e-font-monospace: var(--pf2e-font-monospace-theme), var(--pf2e-font-monospace-default);\n  --pf2e-font-headers-default: \"Nunito\", sans-serif;\n  --pf2e-font-headers-theme: \"Poppins\", \"Montserrat\", \"Roboto\", \"Open Sans\", \"Helvetica Neue\", sans-serif;\n  --pf2e-font-headers: var(--pf2e-font-headers-default), var(--pf2e-font-headers-theme);\n  --pf2e-black-text-rgb: 38, 38, 38;\n  --pf2e-black-text: #262626;\n  --pf2e-blue-headers-rgb: 11, 37, 96;\n  --pf2e-blue-headers: #0b2560;\n  --pf2e-red-headers-rgb: 85, 12, 6;\n  --pf2e-red-headers: #550c06;\n  --pf2e-light-brown-headers: #9f6a57;\n  --pf2e-light-brown-headers-rgb: 159, 106, 87;\n  --pf2e-beige-table-even-rows-rgb: 244, 239, 226;\n  --pf2e-beige-table-even-rows: #f4efe2;\n  --pf2e-tan-table-odd-rows-rgb: 236, 228, 203;\n  --pf2e-tan-table-odd-rows: #ece4cb;\n  --pf2e-brown-table-borders: #9c7566;\n  --pf2e-brown-table-borders-rgb: 156, 117, 102;\n  --pf2e-header-one: rgba(209, 30, 50, 1);\n  --pf2e-header-two: rgba(72, 128, 255, 1);\n  --pf2e-header-three: rgba(209, 30, 50, 1);\n  --pf2e-header-four: rgba(226, 151, 124, 1);\n  --scrollbar-active-thumb-bg: rgba(204, 204, 204, 1);\n  --scrollbar-hover: rgba(51, 51, 51, 1);\n  --scrollbar-thumb-bg: rgba(102, 102, 102, 1);\n  --pf2e-purple-link: rgb(222, 145, 255);\n  --pf2e-blue-link: rgb(64, 105, 157);\n  --pf2e-green-link: rgb(154, 205, 50);\n}\n\n.theme-light {\n  --pf2e-black-text-rgb: 38, 38, 38;\n  --pf2e-black-text: #262626;\n  --pf2e-blue-headers-rgb: 11, 37, 96;\n  --pf2e-blue-headers: #0b2560;\n  --pf2e-red-headers-rgb: 85, 12, 6;\n  --pf2e-red-headers: #550c06;\n  --pf2e-light-brown-headers: #764f41;\n  --pf2e-light-brown-headers-rgb: 118, 79, 65;\n  --pf2e-beige-table-even-rows-rgb: 244, 239, 226;\n  --pf2e-beige-table-even-rows: #f4efe2;\n  --pf2e-tan-table-odd-rows-rgb: 236, 228, 203;\n  --pf2e-tan-table-odd-rows: #ece4cb;\n  --pf2e-brown-table-borders: var(--pf2e-light-brown-headers);\n  --pf2e-brown-table-borders-rgb: rgb(var(--pf2e-light-brown-headers-rgb));\n  --pf2e-table-hover-color: 241, 167, 162;\n  --scrollbar-active-thumb-bg: rgba(102, 102, 102, 1);\n  --scrollbar-hover: rgba(204, 204, 204, 1);\n  --scrollbar-thumb-bg: rgba(153, 153, 153, 1);\n  --pf2e-purple-link: rgb(107, 0, 153);\n  --pf2e-blue-link: rgb(0, 73, 97);\n  --pf2e-green-link: rgb(19, 138, 6);\n}\n\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n/*! General Settings for #Action & [hrefs]s */\na.internal-link[href$=\"#Actions\"][title=\"Single Action\"] {\n  background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 2128 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1022 512L510 0 263 247l265 265-265 265 247 247 512-512zM185 329L2 512l183 183 183-183-183-183z'/%3E%3C/svg%3E%0A\");\n  color: transparent;\n  background-size: cover;\n  background-repeat: no-repeat;\n  display: inline-block;\n  vertical-align: sub;\n  width: 1em;\n  height: 1em;\n}\nh1 a.internal-link[href$=\"#Actions\"][title=\"Single Action\"] {\n  color: transparent;\n  height: 1.375em;\n  width: 1.375em;\n}\na.internal-link[href$=\"#Actions\"][title=\"Single Action\"]:hover {\n  color: transparent;\n}\nli a.internal-link[href$=\"#Actions\"][title=\"Single Action\"] {\n  vertical-align: middle;\n}\n.theme-dark a.internal-link[href$=\"#Actions\"][title=\"Single Action\"] {\n  background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 2128 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1022 512L510 0 263 247l265 265-265 265 247 247 512-512zM185 329L2 512l183 183 183-183-183-183z' fill='red'/%3E%3C/svg%3E%0A\");\n}\nh1 a.internal-link[href$=\"#Actions\"][title=\"Single Action\"] {\n  vertical-align: middle;\n}\n\na.internal-link[href$=\"#Actions\"][title=Two-Action] {\n  background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 2128 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1018 512L507 1 261 248l264 264-264 264 246 247 511-511zM183 329L0 512l183 183 183-183-183-183zm1417 175L1152 56 935 272l232 232-232 232 217 217 448-449z'/%3E%3C/svg%3E\");\n  color: transparent;\n  background-size: cover;\n  background-repeat: no-repeat;\n  display: inline-block;\n  vertical-align: baseline;\n  width: 1.6em;\n  height: 1em;\n}\nh1 a.internal-link[href$=\"#Actions\"][title=Two-Action] {\n  color: transparent;\n  height: 1.375em;\n  width: 1.375em;\n}\na.internal-link[href$=\"#Actions\"][title=Two-Action]:hover {\n  color: transparent;\n}\nli a.internal-link[href$=\"#Actions\"][title=Two-Action] {\n  vertical-align: middle;\n}\nh1 a.internal-link[href$=\"#Actions\"][title=Two-Action],\na.internal-link[href$=\"#Actions\"][title=Two-Action] .callout-title-inner {\n  width: 1.6em;\n}\n.theme-dark a.internal-link[href$=\"#Actions\"][title=Two-Action] {\n  background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 2128 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1018 512L507 1 261 248l264 264-264 264 246 247 511-511zM183 329L0 512l183 183 183-183-183-183zm1417 175L1152 56 935 272l232 232-232 232 217 217 448-449z' fill='red'/%3E%3C/svg%3E\");\n}\n\na.internal-link[href$=\"#Actions\"][title=Three-Action] {\n  background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 2128 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m1025 512-512-512-247 247 265 265-265 265 247 247zm1098-10-360-360-175 174 187 186-187 187 175 174zm-1935-173-183 183 183 183 183-183zm1427 175-449-449-218 217 233 232-233 233 218 217z'/%3E%3C/svg%3E\");\n  vertical-align: baseline;\n  width: 2.1em;\n  height: 1em;\n  color: transparent;\n  background-size: cover;\n  background-repeat: no-repeat;\n  display: inline-block;\n}\nh1 a.internal-link[href$=\"#Actions\"][title=Three-Action] {\n  color: transparent;\n  height: 1.375em;\n  width: 1.375em;\n}\na.internal-link[href$=\"#Actions\"][title=Three-Action]:hover {\n  color: transparent;\n}\nli a.internal-link[href$=\"#Actions\"][title=Three-Action] {\n  vertical-align: middle;\n}\nh1 a.internal-link[href$=\"#Actions\"][title=Three-Action],\na.internal-link[href$=\"#Actions\"][title=Three-Action] .callout-title-inner {\n  width: 2.9em;\n  vertical-align: text-top;\n}\n.theme-dark a.internal-link[href$=\"#Actions\"][title=Three-Action] {\n  background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 2128 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m1025 512-512-512-247 247 265 265-265 265 247 247zm1098-10-360-360-175 174 187 186-187 187 175 174zm-1935-173-183 183 183 183 183-183zm1427 175-449-449-218 217 233 232-233 233 218 217z' fill='red' /%3E%3C/svg%3E\");\n}\n\na.internal-link[href$=\"#Actions\"][title=Reaction] {\n  background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 2128 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1013 377q-23-71-82-128-60-57-143.5-94T603 105q-101-14-207 1-75 11-140 35t-116 58q-51 34-87 77-36 42-53 89 21-26 50-48 29-23 64.5-41t76.5-31q42-13 88-20 85-12 166-1t148.5 40.5Q661 294 709 340t66 103q18 58 2 113-17 55-60.5 100.5T605 735q-67 32-152 44-28 4-55.5 5.5t-54.5.5q63 14 131.5 17t137.5-7q106-15 190-56 84-40 138-96.5t75-125.5q20-68-2-140zM152 823l489 103-109-149 56-215-436 261z'/%3E%3C/svg%3E\");\n  color: transparent;\n  background-size: cover;\n  background-repeat: no-repeat;\n  display: inline-block;\n  vertical-align: middle;\n  width: 1em;\n  height: 1em;\n}\nh1 a.internal-link[href$=\"#Actions\"][title=Reaction] {\n  color: transparent;\n  height: 1.375em;\n  width: 1.375em;\n}\na.internal-link[href$=\"#Actions\"][title=Reaction]:hover {\n  color: transparent;\n}\nli a.internal-link[href$=\"#Actions\"][title=Reaction] {\n  vertical-align: middle;\n}\nh1 a.internal-link[href$=\"#Actions\"][title=Reaction],\na.internal-link[href$=\"#Actions\"][title=Reaction] .callout-title-inner {\n  vertical-align: text-top;\n}\n.theme-dark a.internal-link[href$=\"#Actions\"][title=Reaction] {\n  background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 2128 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1013 377q-23-71-82-128-60-57-143.5-94T603 105q-101-14-207 1-75 11-140 35t-116 58q-51 34-87 77-36 42-53 89 21-26 50-48 29-23 64.5-41t76.5-31q42-13 88-20 85-12 166-1t148.5 40.5Q661 294 709 340t66 103q18 58 2 113-17 55-60.5 100.5T605 735q-67 32-152 44-28 4-55.5 5.5t-54.5.5q63 14 131.5 17t137.5-7q106-15 190-56 84-40 138-96.5t75-125.5q20-68-2-140zM152 823l489 103-109-149 56-215-436 261z' fill='red'/%3E%3C/svg%3E\");\n}\n\na.internal-link[href$=\"#Actions\"][title=\"Free Action\"] {\n  background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 2128 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M516 0L267 250 0 518l509 506 515-515L516 0zM188 496l109-109 109 109-109 109-109-109zm311 380L386 763l251-251-245-246 124-124 359 359-376 375z'/%3E%3C/svg%3E\");\n  color: transparent;\n  background-size: cover;\n  background-repeat: no-repeat;\n  display: inline-block;\n  vertical-align: baseline;\n  width: 1em;\n  height: 1em;\n}\nh1 a.internal-link[href$=\"#Actions\"][title=\"Free Action\"] {\n  color: transparent;\n  height: 1.375em;\n  width: 1.375em;\n}\na.internal-link[href$=\"#Actions\"][title=\"Free Action\"]:hover {\n  color: transparent;\n}\nli a.internal-link[href$=\"#Actions\"][title=\"Free Action\"] {\n  vertical-align: middle;\n}\n.theme-dark a.internal-link[href$=\"#Actions\"][title=\"Free Action\"] {\n  background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 2128 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M516 0L267 250 0 518l509 506 515-515L516 0zM188 496l109-109 109 109-109 109-109-109zm311 380L386 763l251-251-245-246 124-124 359 359-376 375z' fill='red'/%3E%3C/svg%3E\");\n}\n\na.internal-link[href$=\"#Actions\"][title=Varies] {\n  background-image: url(\"data:image/svg+xml,%3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Ctitle%3Eload%3C/title%3E%3Cpath d='M16 2c-7.732 0-14 6.268-14 14s6.268 14 14 14 14-6.268 14-14-6.268-14-14-14zM28.998 15.952c-3.808 1.889-8.19 1.875-11.971 0 3.625-2.404 5.958-6.273 6.398-10.608 3.352 2.343 5.557 6.22 5.573 10.608zM22.46 4.735c-0.273 4.234-2.484 8.022-5.989 10.353-0.268-4.347-2.447-8.296-5.983-10.845 1.677-0.789 3.54-1.243 5.512-1.243 2.352 0 4.555 0.638 6.46 1.735zM9.471 4.776c3.532 2.354 5.708 6.151 5.974 10.359-3.9-1.941-8.413-2.028-12.389-0.238 0.365-4.322 2.853-8.040 6.415-10.121zM3.002 16.047c3.808-1.887 8.192-1.873 11.972 0.001-3.628 2.405-5.96 6.27-6.4 10.607-3.352-2.343-5.556-6.22-5.572-10.608zM9.542 27.266c0.273-4.236 2.478-8.021 5.987-10.354 0.268 4.352 2.447 8.295 5.984 10.844-1.676 0.79-3.54 1.244-5.513 1.244-2.352 0-4.553-0.637-6.458-1.734zM22.531 27.222c-3.534-2.354-5.711-6.145-5.976-10.359 2.035 1.013 4.237 1.523 6.442 1.523 2.025 0 4.045-0.455 5.949-1.314-0.357 4.334-2.845 8.064-6.415 10.15z'%3E%3C/path%3E%3C/svg%3E\");\n  color: transparent;\n  background-size: cover;\n  background-repeat: no-repeat;\n  display: inline-block;\n  vertical-align: baseline;\n  width: 1em;\n  height: 1em;\n}\nh1 a.internal-link[href$=\"#Actions\"][title=Varies] {\n  color: transparent;\n  height: 1.375em;\n  width: 1.375em;\n}\na.internal-link[href$=\"#Actions\"][title=Varies]:hover {\n  color: transparent;\n}\nli a.internal-link[href$=\"#Actions\"][title=Varies] {\n  vertical-align: middle;\n}\n.theme-dark a.internal-link[href$=\"#Actions\"][title=Varies] {\n  background-image: url(\"data:image/svg+xml,%3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Ctitle%3Eload%3C/title%3E%3Cpath d='M16 2c-7.732 0-14 6.268-14 14s6.268 14 14 14 14-6.268 14-14-6.268-14-14-14zM28.998 15.952c-3.808 1.889-8.19 1.875-11.971 0 3.625-2.404 5.958-6.273 6.398-10.608 3.352 2.343 5.557 6.22 5.573 10.608zM22.46 4.735c-0.273 4.234-2.484 8.022-5.989 10.353-0.268-4.347-2.447-8.296-5.983-10.845 1.677-0.789 3.54-1.243 5.512-1.243 2.352 0 4.555 0.638 6.46 1.735zM9.471 4.776c3.532 2.354 5.708 6.151 5.974 10.359-3.9-1.941-8.413-2.028-12.389-0.238 0.365-4.322 2.853-8.040 6.415-10.121zM3.002 16.047c3.808-1.887 8.192-1.873 11.972 0.001-3.628 2.405-5.96 6.27-6.4 10.607-3.352-2.343-5.556-6.22-5.572-10.608zM9.542 27.266c0.273-4.236 2.478-8.021 5.987-10.354 0.268 4.352 2.447 8.295 5.984 10.844-1.676 0.79-3.54 1.244-5.513 1.244-2.352 0-4.553-0.637-6.458-1.734zM22.531 27.222c-3.534-2.354-5.711-6.145-5.976-10.359 2.035 1.013 4.237 1.523 6.442 1.523 2.025 0 4.045-0.455 5.949-1.314-0.357 4.334-2.845 8.064-6.415 10.15z' fill='red'%3E%3C/path%3E%3C/svg%3E%0A\");\n}\n\na.internal-link[href$=\"#Actions\"][title=\"Duration or Frequency\"] {\n  background-image: url(\"data:image/svg+xml,%3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Ctitle%3Ehour-glass%3C/title%3E%3Cpath d='M15.6 4.576c0-2.139 0-2.348 0-2.348 0-0.789-2.508-2.228-5.6-2.228-3.093 0-5.6 1.439-5.6 2.228 0 0 0 0.209 0 2.348 0 2.141 3.877 3.908 3.877 5.424 0 1.514-3.877 3.281-3.877 5.422s0 2.35 0 2.35c0 0.788 2.507 2.228 5.6 2.228 3.092 0 5.6-1.44 5.6-2.229 0 0 0-0.209 0-2.35s-3.877-3.908-3.877-5.422c0-1.515 3.877-3.282 3.877-5.423zM5.941 2.328c0.696-0.439 2-1.082 4.114-1.082s4.006 1.082 4.006 1.082c0.142 0.086 0.698 0.383 0.317 0.609-0.838 0.497-2.478 1.020-4.378 1.020s-3.484-0.576-4.324-1.074c-0.381-0.225 0.265-0.555 0.265-0.555zM10.501 10c0 1.193 0.996 1.961 2.051 2.986 0.771 0.748 1.826 1.773 1.826 2.435v1.328c-0.97-0.483-3.872-0.955-3.872-2.504 0-0.783-1.013-0.783-1.013 0 0 1.549-2.902 2.021-3.872 2.504v-1.328c0-0.662 1.056-1.688 1.826-2.435 1.055-1.025 2.051-1.793 2.051-2.986s-0.996-1.961-2.051-2.986c-0.771-0.75-1.826-1.775-1.826-2.438l-0.046-0.998c1.026 0.553 2.652 1.078 4.425 1.078 1.772 0 3.406-0.525 4.433-1.078l-0.055 0.998c0 0.662-1.056 1.688-1.826 2.438-1.054 1.025-2.051 1.793-2.051 2.986z'%3E%3C/path%3E%3C/svg%3E%0A\");\n  color: transparent;\n  background-size: cover;\n  background-repeat: no-repeat;\n  display: inline-block;\n  vertical-align: baseline;\n  width: 1em;\n  height: 1em;\n}\nh1 a.internal-link[href$=\"#Actions\"][title=\"Duration or Frequency\"] {\n  color: transparent;\n  height: 1.375em;\n  width: 1.375em;\n}\na.internal-link[href$=\"#Actions\"][title=\"Duration or Frequency\"]:hover {\n  color: transparent;\n}\nli a.internal-link[href$=\"#Actions\"][title=\"Duration or Frequency\"] {\n  vertical-align: middle;\n}\n.theme-dark a.internal-link[href$=\"#Actions\"][title=\"Duration or Frequency\"] {\n  background-image: url(\"data:image/svg+xml,%3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Ctitle%3Ehour-glass%3C/title%3E%3Cpath d='M15.6 4.576c0-2.139 0-2.348 0-2.348 0-0.789-2.508-2.228-5.6-2.228-3.093 0-5.6 1.439-5.6 2.228 0 0 0 0.209 0 2.348 0 2.141 3.877 3.908 3.877 5.424 0 1.514-3.877 3.281-3.877 5.422s0 2.35 0 2.35c0 0.788 2.507 2.228 5.6 2.228 3.092 0 5.6-1.44 5.6-2.229 0 0 0-0.209 0-2.35s-3.877-3.908-3.877-5.422c0-1.515 3.877-3.282 3.877-5.423zM5.941 2.328c0.696-0.439 2-1.082 4.114-1.082s4.006 1.082 4.006 1.082c0.142 0.086 0.698 0.383 0.317 0.609-0.838 0.497-2.478 1.020-4.378 1.020s-3.484-0.576-4.324-1.074c-0.381-0.225 0.265-0.555 0.265-0.555zM10.501 10c0 1.193 0.996 1.961 2.051 2.986 0.771 0.748 1.826 1.773 1.826 2.435v1.328c-0.97-0.483-3.872-0.955-3.872-2.504 0-0.783-1.013-0.783-1.013 0 0 1.549-2.902 2.021-3.872 2.504v-1.328c0-0.662 1.056-1.688 1.826-2.435 1.055-1.025 2.051-1.793 2.051-2.986s-0.996-1.961-2.051-2.986c-0.771-0.75-1.826-1.775-1.826-2.438l-0.046-0.998c1.026 0.553 2.652 1.078 4.425 1.078 1.772 0 3.406-0.525 4.433-1.078l-0.055 0.998c0 0.662-1.056 1.688-1.826 2.438-1.054 1.025-2.051 1.793-2.051 2.986z' fill='red'%3E%3C/path%3E%3C/svg%3E%0A\");\n}\n\n.callout[data-callout=statblock-pf2e] a.internal-link[href$=\"#Actions\"][title=\"Single Action\"] {\n  background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 2040 1024'%3E%3Cpath d='M1022 512L510 0 263 247l265 265-265 265 247 247 512-512zM185 329L2 512l183 183 183-183-183-183z'/%3E%3C/svg%3E\");\n  background-size: cover;\n  background-repeat: no-repeat;\n  display: inline-block;\n  vertical-align: top;\n  width: 1em;\n  height: 1em;\n  content: \"\";\n  color: transparent;\n}\n.callout[data-callout=statblock-pf2e] a.internal-link[href$=\"#Actions\"][title=Two-Action] {\n  background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 2040 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1018 512L507 1 261 248l264 264-264 264 246 247 511-511zM183 329L0 512l183 183 183-183-183-183zm1417 175L1152 56 935 272l232 232-232 232 217 217 448-449z'/%3E%3C/svg%3E\");\n  background-repeat: no-repeat;\n  background-size: 1.5625rem;\n  line-height: 1.5625rem;\n  vertical-align: top;\n  width: 1em;\n  height: 1em;\n  content: \"\";\n  color: transparent;\n}\n.callout[data-callout=statblock-pf2e] a.internal-link[href$=\"#Actions\"][title=Three-Action] {\n  background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 2128 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m1025 512-512-512-247 247 265 265-265 265 247 247zm1098-10-360-360-175 174 187 186-187 187 175 174zm-1935-173-183 183 183 183 183-183zm1427 175-449-449-218 217 233 232-233 233 218 217z'/%3E%3C/svg%3E\");\n  background-repeat: no-repeat;\n  line-height: 1.5625rem;\n  vertical-align: top;\n  width: 1em;\n  height: 1em;\n  content: \"\";\n  color: transparent;\n}\n.callout[data-callout=statblock-pf2e] a.internal-link[href$=\"#Actions\"][title=Reaction] {\n  background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 2128 1024'%3E%3Cpath d='M1013 377q-23-71-82-128-60-57-143.5-94T603 105q-101-14-207 1-75 11-140 35t-116 58q-51 34-87 77-36 42-53 89 21-26 50-48 29-23 64.5-41t76.5-31q42-13 88-20 85-12 166-1t148.5 40.5Q661 294 709 340t66 103q18 58 2 113-17 55-60.5 100.5T605 735q-67 32-152 44-28 4-55.5 5.5t-54.5.5q63 14 131.5 17t137.5-7q106-15 190-56 84-40 138-96.5t75-125.5q20-68-2-140zM152 823l489 103-109-149 56-215-436 261z'/%3E%3C/svg%3E\");\n  background-size: cover;\n  display: inline-block;\n  vertical-align: middle;\n  width: 1em;\n  height: 1em;\n  content: \"\";\n  color: transparent;\n}\n.callout[data-callout=statblock-pf2e] a.internal-link[href$=\"#Actions\"][title=\"Free Action\"] {\n  background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 2128 1024'%3E%3Cpath d='M516 0L267 250 0 518l509 506 515-515L516 0zM188 496l109-109 109 109-109 109-109-109zm311 380L386 763l251-251-245-246 124-124 359 359-376 375z'/%3E%3C/svg%3E\");\n  background-size: cover;\n  display: inline-block;\n  vertical-align: top;\n  width: 1em;\n  height: 1em;\n  content: \"\";\n  color: transparent;\n}\n.callout[data-callout=statblock-pf2e] a.internal-link[href$=\"#Actions\"][title=Varies] {\n  background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpath d='M16 2c-7.732 0-14 6.268-14 14s6.268 14 14 14 14-6.268 14-14-6.268-14-14-14zM28.998 15.952c-3.808 1.889-8.19 1.875-11.971 0 3.625-2.404 5.958-6.273 6.398-10.608 3.352 2.343 5.557 6.22 5.573 10.608zM22.46 4.735c-0.273 4.234-2.484 8.022-5.989 10.353-0.268-4.347-2.447-8.296-5.983-10.845 1.677-0.789 3.54-1.243 5.512-1.243 2.352 0 4.555 0.638 6.46 1.735zM9.471 4.776c3.532 2.354 5.708 6.151 5.974 10.359-3.9-1.941-8.413-2.028-12.389-0.238 0.365-4.322 2.853-8.040 6.415-10.121zM3.002 16.047c3.808-1.887 8.192-1.873 11.972 0.001-3.628 2.405-5.96 6.27-6.4 10.607-3.352-2.343-5.556-6.22-5.572-10.608zM9.542 27.266c0.273-4.236 2.478-8.021 5.987-10.354 0.268 4.352 2.447 8.295 5.984 10.844-1.676 0.79-3.54 1.244-5.513 1.244-2.352 0-4.553-0.637-6.458-1.734zM22.531 27.222c-3.534-2.354-5.711-6.145-5.976-10.359 2.035 1.013 4.237 1.523 6.442 1.523 2.025 0 4.045-0.455 5.949-1.314-0.357 4.334-2.845 8.064-6.415 10.15z'%3E%3C/path%3E%3C/svg%3E\");\n  background-size: cover;\n  display: inline-block;\n  vertical-align: top;\n  width: 1em;\n  height: 1em;\n  content: \"\";\n  color: transparent;\n}\n.callout[data-callout=statblock-pf2e] a.internal-link[href$=\"#Actions\"][title=\"Duration or Frequency\"] {\n  background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' version='1.1' width='20' height='20' viewBox='0 0 20 20'%3E%3Ctitle%3Ehour-glass%3C/title%3E%3Cpath d='M15.6 4.576c0-2.139 0-2.348 0-2.348 0-0.789-2.508-2.228-5.6-2.228-3.093 0-5.6 1.439-5.6 2.228 0 0 0 0.209 0 2.348 0 2.141 3.877 3.908 3.877 5.424 0 1.514-3.877 3.281-3.877 5.422s0 2.35 0 2.35c0 0.788 2.507 2.228 5.6 2.228 3.092 0 5.6-1.44 5.6-2.229 0 0 0-0.209 0-2.35s-3.877-3.908-3.877-5.422c0-1.515 3.877-3.282 3.877-5.423zM5.941 2.328c0.696-0.439 2-1.082 4.114-1.082s4.006 1.082 4.006 1.082c0.142 0.086 0.698 0.383 0.317 0.609-0.838 0.497-2.478 1.020-4.378 1.020s-3.484-0.576-4.324-1.074c-0.381-0.225 0.265-0.555 0.265-0.555zM10.501 10c0 1.193 0.996 1.961 2.051 2.986 0.771 0.748 1.826 1.773 1.826 2.435v1.328c-0.97-0.483-3.872-0.955-3.872-2.504 0-0.783-1.013-0.783-1.013 0 0 1.549-2.902 2.021-3.872 2.504v-1.328c0-0.662 1.056-1.688 1.826-2.435 1.055-1.025 2.051-1.793 2.051-2.986s-0.996-1.961-2.051-2.986c-0.771-0.75-1.826-1.775-1.826-2.438l-0.046-0.998c1.026 0.553 2.652 1.078 4.425 1.078 1.772 0 3.406-0.525 4.433-1.078l-0.055 0.998c0 0.662-1.056 1.688-1.826 2.438-1.054 1.025-2.051 1.793-2.051 2.986z'%3E%3C/path%3E%3C/svg%3E%0A\");\n  background-size: cover;\n  display: inline-block;\n  vertical-align: top;\n  width: 1em;\n  height: 1em;\n  content: \"\";\n  color: transparent;\n}\n\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\na.internal-link[title=\"Common Rarity Trait\"] {\n  background: rgb(232, 232, 232);\n  border-color: rgb(232, 232, 232);\n  background-clip: border-box;\n  border-radius: 0.3125rem;\n  border-style: solid;\n  color: rgb(0, 0, 0) !important;\n  cursor: help;\n  font-weight: 700;\n  padding: 0.0625rem;\n  margin: 0.125rem;\n  text-align: center;\n  text-indent: 0;\n  word-break: keep-all;\n  font-style: normal;\n  display: inline-block;\n  border-width: 0.0625rem;\n}\nli a.internal-link[title=\"Common Rarity Trait\"] {\n  margin-bottom: 0;\n}\n.callout[data-callout=statblock-pf2e] a.internal-link[title=\"Common Rarity Trait\"] {\n  margin-bottom: 0.75rem;\n}\na.internal-link[title=\"Uncommon Rarity Trait\"] {\n  background: rgb(152, 81, 61);\n  border-color: rgb(152, 81, 61);\n  background-clip: border-box;\n  border-radius: 0.3125rem;\n  border-style: solid;\n  color: rgb(232, 232, 232) !important;\n  cursor: help;\n  font-weight: 700;\n  padding: 0.0625rem;\n  margin: 0.125rem;\n  text-align: center;\n  text-indent: 0;\n  word-break: keep-all;\n  font-style: normal;\n  display: inline-block;\n  border-width: 0.0625rem;\n}\nli a.internal-link[title=\"Uncommon Rarity Trait\"] {\n  margin-bottom: 0;\n}\n.callout[data-callout=statblock-pf2e] a.internal-link[title=\"Uncommon Rarity Trait\"] {\n  margin-bottom: 0.75rem;\n}\na.internal-link[title=\"Rare Rarity Trait\"] {\n  background: rgb(0, 38, 100);\n  border-color: rgb(0, 38, 100);\n  background-clip: border-box;\n  border-radius: 0.3125rem;\n  border-style: solid;\n  color: rgb(232, 232, 232) !important;\n  cursor: help;\n  font-weight: 700;\n  padding: 0.0625rem;\n  margin: 0.125rem;\n  text-align: center;\n  text-indent: 0;\n  word-break: keep-all;\n  font-style: normal;\n  display: inline-block;\n  border-width: 0.0625rem;\n}\nli a.internal-link[title=\"Rare Rarity Trait\"] {\n  margin-bottom: 0;\n}\n.callout[data-callout=statblock-pf2e] a.internal-link[title=\"Rare Rarity Trait\"] {\n  margin-bottom: 0.75rem;\n}\na.internal-link[title=\"Unique Rarity Trait\"] {\n  background: rgb(84, 22, 110);\n  border-color: rgb(84, 22, 110);\n  background-clip: border-box;\n  border-radius: 0.3125rem;\n  border-style: solid;\n  color: rgb(232, 232, 232) !important;\n  cursor: help;\n  font-weight: 700;\n  padding: 0.0625rem;\n  margin: 0.125rem;\n  text-align: center;\n  text-indent: 0;\n  word-break: keep-all;\n  font-style: normal;\n  display: inline-block;\n  border-width: 0.0625rem;\n}\nli a.internal-link[title=\"Unique Rarity Trait\"] {\n  margin-bottom: 0;\n}\n.callout[data-callout=statblock-pf2e] a.internal-link[title=\"Unique Rarity Trait\"] {\n  margin-bottom: 0.75rem;\n}\na.internal-link[title*=\"Alignment Trait\"] {\n  background: rgb(102, 111, 153);\n  border-color: rgb(102, 111, 153);\n  background-clip: border-box;\n  border-radius: 0.3125rem;\n  border-style: solid;\n  color: rgb(232, 232, 232) !important;\n  cursor: help;\n  font-weight: 700;\n  padding: 0.0625rem;\n  margin: 0.125rem;\n  text-align: center;\n  text-indent: 0;\n  word-break: keep-all;\n  font-style: normal;\n  display: inline-block;\n  border-width: 0.0625rem;\n}\nli a.internal-link[title*=\"Alignment Trait\"] {\n  margin-bottom: 0;\n}\n.callout[data-callout=statblock-pf2e] a.internal-link[title*=\"Alignment Trait\"] {\n  margin-bottom: 0.75rem;\n}\na.internal-link[title*=\"Size Trait\"] {\n  background: rgb(82, 122, 95);\n  border-color: rgb(82, 122, 95);\n  background-clip: border-box;\n  border-radius: 0.3125rem;\n  border-style: solid;\n  color: rgb(232, 232, 232) !important;\n  cursor: help;\n  font-weight: 700;\n  padding: 0.0625rem;\n  margin: 0.125rem;\n  text-align: center;\n  text-indent: 0;\n  word-break: keep-all;\n  font-style: normal;\n  display: inline-block;\n  border-width: 0.0625rem;\n}\nli a.internal-link[title*=\"Size Trait\"] {\n  margin-bottom: 0;\n}\n.callout[data-callout=statblock-pf2e] a.internal-link[title*=\"Size Trait\"] {\n  margin-bottom: 0.75rem;\n}\na.internal-link[title*=\"Action & Ability Trait\"], a.internal-link[title*=\"Ancestry & Heritage Trait\"], a.internal-link[title*=\"Armor Trait\"], a.internal-link[title*=\"Class Trait\"], a.internal-link[title*=\"Combat Trait\"], a.internal-link[title*=\"Creature Trait\"], a.internal-link[title*=\"Creature Type Trait\"], a.internal-link[title*=\"Effect Trait\"], a.internal-link[title*=\"Element Trait\"], a.internal-link[title*=\"Equipment Trait\"], a.internal-link[title*=\"Feat Trait\"], a.internal-link[title*=\"General Trait\"], a.internal-link[title*=\"Gravity Trait\"], a.internal-link[title*=\"Hazard Trait\"], a.internal-link[title*=\"Item Trait\"], a.internal-link[title*=\"Morphic Trait\"], a.internal-link[title*=\"Planar Trait\"], a.internal-link[title*=\"Settlement Trait\"], a.internal-link[title*=\"School Trait\"], a.internal-link[title*=\"Spell Trait\"], a.internal-link[title*=\"Tradition Trait\"], a.internal-link[title*=\"Weapon Trait\"] {\n  background: rgb(97, 20, 5);\n  border-color: rgb(97, 20, 5);\n  background-clip: border-box;\n  border-radius: 0.3125rem;\n  border-style: solid;\n  color: rgb(232, 232, 232) !important;\n  cursor: help;\n  font-weight: 700;\n  padding: 0.0625rem;\n  margin: 0.125rem;\n  text-align: center;\n  text-indent: 0;\n  word-break: keep-all;\n  font-style: normal;\n  display: inline-block;\n  border-width: 0.0625rem;\n}\nli a.internal-link[title*=\"Action & Ability Trait\"], li a.internal-link[title*=\"Ancestry & Heritage Trait\"], li a.internal-link[title*=\"Armor Trait\"], li a.internal-link[title*=\"Class Trait\"], li a.internal-link[title*=\"Combat Trait\"], li a.internal-link[title*=\"Creature Trait\"], li a.internal-link[title*=\"Creature Type Trait\"], li a.internal-link[title*=\"Effect Trait\"], li a.internal-link[title*=\"Element Trait\"], li a.internal-link[title*=\"Equipment Trait\"], li a.internal-link[title*=\"Feat Trait\"], li a.internal-link[title*=\"General Trait\"], li a.internal-link[title*=\"Gravity Trait\"], li a.internal-link[title*=\"Hazard Trait\"], li a.internal-link[title*=\"Item Trait\"], li a.internal-link[title*=\"Morphic Trait\"], li a.internal-link[title*=\"Planar Trait\"], li a.internal-link[title*=\"Settlement Trait\"], li a.internal-link[title*=\"School Trait\"], li a.internal-link[title*=\"Spell Trait\"], li a.internal-link[title*=\"Tradition Trait\"], li a.internal-link[title*=\"Weapon Trait\"] {\n  margin-bottom: 0;\n}\n.callout[data-callout=statblock-pf2e] a.internal-link[title*=\"Action & Ability Trait\"], .callout[data-callout=statblock-pf2e] a.internal-link[title*=\"Ancestry & Heritage Trait\"], .callout[data-callout=statblock-pf2e] a.internal-link[title*=\"Armor Trait\"], .callout[data-callout=statblock-pf2e] a.internal-link[title*=\"Class Trait\"], .callout[data-callout=statblock-pf2e] a.internal-link[title*=\"Combat Trait\"], .callout[data-callout=statblock-pf2e] a.internal-link[title*=\"Creature Trait\"], .callout[data-callout=statblock-pf2e] a.internal-link[title*=\"Creature Type Trait\"], .callout[data-callout=statblock-pf2e] a.internal-link[title*=\"Effect Trait\"], .callout[data-callout=statblock-pf2e] a.internal-link[title*=\"Element Trait\"], .callout[data-callout=statblock-pf2e] a.internal-link[title*=\"Equipment Trait\"], .callout[data-callout=statblock-pf2e] a.internal-link[title*=\"Feat Trait\"], .callout[data-callout=statblock-pf2e] a.internal-link[title*=\"General Trait\"], .callout[data-callout=statblock-pf2e] a.internal-link[title*=\"Gravity Trait\"], .callout[data-callout=statblock-pf2e] a.internal-link[title*=\"Hazard Trait\"], .callout[data-callout=statblock-pf2e] a.internal-link[title*=\"Item Trait\"], .callout[data-callout=statblock-pf2e] a.internal-link[title*=\"Morphic Trait\"], .callout[data-callout=statblock-pf2e] a.internal-link[title*=\"Planar Trait\"], .callout[data-callout=statblock-pf2e] a.internal-link[title*=\"Settlement Trait\"], .callout[data-callout=statblock-pf2e] a.internal-link[title*=\"School Trait\"], .callout[data-callout=statblock-pf2e] a.internal-link[title*=\"Spell Trait\"], .callout[data-callout=statblock-pf2e] a.internal-link[title*=\"Tradition Trait\"], .callout[data-callout=statblock-pf2e] a.internal-link[title*=\"Weapon Trait\"] {\n  margin-bottom: 0.75rem;\n}\n\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n:is(.pf2e) .markdown-embed {\n  border: none;\n  border-radius: 0;\n  margin: 0;\n  padding: 0;\n  position: relative;\n}\n:is(.pf2e) .markdown-embed .embed-title,\n:is(.pf2e) .markdown-embed .mod-header {\n  display: none;\n  gap: 0;\n  padding: 0;\n}\n:is(.pf2e) .markdown-embed-link svg,\n:is(.pf2e) .markdown-embed .file-embed-link {\n  height: 18px;\n  width: 18px;\n  right: 0;\n  top: 0;\n  text-align: center;\n  vertical-align: middle;\n}\n:is(.pf2e) .markdown-embed-source-view.invisible-embed, :is(.pf2e) .markdown-embed-rendered.invisible-embed {\n  --embed-border-left: 0;\n  --embed-border-right: 0;\n  --embed-padding: 0;\n}\n:is(.pf2e) .markdown-embed .markdown-embed-content {\n  max-height: unset;\n  overflow: unset;\n}\n:is(.pf2e) .markdown-embed .markdown-embed-content > .markdown-preview-view {\n  overflow-y: unset;\n}\n:is(.pf2e) .markdown-source-view.internal-embed.markdown-embed-title {\n  border: none;\n  border-radius: 0;\n  margin: 0;\n  padding: 0;\n  position: relative;\n  display: none;\n  gap: 0;\n  font-size: 0;\n}\n\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n:is(.pf2e) a.internal-link {\n  text-decoration: none;\n  color: var(--pf2e-blue-link);\n  filter: brightness(1);\n}\n.theme-dark :is(.pf2e) a.internal-link {\n  filter: brightness(1.2);\n}\n:is(.pf2e) a.internal-link:hover {\n  text-decoration: underline;\n  color: var(--pf2e-green-link);\n}\n:is(.pf2e) .cm-link .cm-underline,\n:is(.pf2e) .cm-url .cm-underline {\n  color: var(--pf2e-blue-link);\n  text-decoration: none;\n}\n:is(.pf2e) .cm-link .cm-underline:hover,\n:is(.pf2e) .cm-url .cm-underline:hover {\n  color: var(--pf2e-green-link);\n  text-decoration: underline;\n}\n:is(.pf2e) .external-link {\n  color: var(--pf2e-green-link);\n  background-image: none;\n  background-size: 0;\n  font-style: italic;\n  padding-right: 0;\n}\n:is(.pf2e) .external-link:hover {\n  color: var(--pf2e-purple-link);\n  text-decoration: underline;\n}\n\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n[class^=Pathfinder-], [class*=\" Pathfinder-\"] {\n  /* use !important to prevent issues with browser extensions that change fonts */\n  font-family: \"Pathfinder\", serif !important;\n  -webkit-font-smoothing: antialiased;\n  font-style: normal;\n  font-variant: normal;\n  font-weight: normal;\n  line-height: 1;\n  /* Better Font Rendering =========== */\n  -moz-osx-font-smoothing: grayscale;\n  text-transform: none;\n}\n\n.pathfinder-1action::before {\n  content: \"⬻\";\n}\n\n.pathfinder-2actions::before {\n  content: \"⬺\";\n}\n\n.pathfinder-3actions::before {\n  content: \"⬽\";\n}\n\n.pathfinder-delay::before {\n  content: \"⭓\";\n}\n\n.pathfinder-reaction::before {\n  content: \"⬲\";\n}\n\n:is(.pf2e):is(.callout, .admonition):hover {\n  scrollbar-color: var(--scrollbar-active-thumb-bg) transparent;\n}\n:is(.pf2e):is(.callout, .admonition)::-webkit-scrollbar {\n  width: 12px;\n}\n:is(.pf2e):is(.callout, .admonition)::-webkit-scrollbar-track {\n  background: var(--scrollbar-hover);\n  border: 0.25rem solid transparent;\n  background-clip: content-box;\n}\n:is(.pf2e):is(.callout, .admonition)::-webkit-scrollbar-thumb {\n  background: var(--scrollbar-active-thumb-bg);\n  border: 0.0625rem solid var(--scrollbar-thumb-bg);\n}\n\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\nsub {\n  font-size: 75%;\n  line-height: 0;\n  position: relative;\n  text-align: center;\n  vertical-align: baseline;\n  bottom: -0.25em;\n}\n\nsup {\n  font-size: 75%;\n  line-height: 0;\n  position: relative;\n  text-align: center;\n  vertical-align: baseline;\n  top: -0.5em;\n}\n\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n:is(.pf2e) .markdown-rendered table {\n  border: 0.1875rem solid var(--pf2e-red-headers);\n}\n:is(.pf2e) .markdown-rendered table a {\n  font-weight: 500;\n  color: var(--pf2e-purple-link);\n}\n:is(.pf2e) .markdown-rendered thead {\n  background-color: var(--pf2e-red-headers);\n  border-bottom: 0.1875rem solid rgba(var(--pf2e-brown-table-borders-rgb), 0.5);\n  color: rgb(232, 232, 232);\n}\n:is(.pf2e) .markdown-rendered tbody {\n  background-color: rgb(var(--pf2e-tan-table-odd-rows-rgb));\n  color: rgb(36, 36, 36);\n}\n:is(.pf2e) .markdown-rendered tbody tr:nth-child(even) {\n  background-color: rgb(var(--pf2e-beige-table-even-rows-rgb));\n}\n:is(.pf2e) .markdown-rendered tbody tr:hover {\n  background-color: rgba(var(--pf2e-table-hover-color), 0.8);\n}\n:is(.pf2e) .markdown-rendered th {\n  background-color: var(--pf2e-red-headers);\n  color: rgb(232, 232, 232);\n}\n:is(.pf2e) .markdown-rendered th:hover {\n  background-color: rgba(var(--pf2e-table-hover-color), 0.8);\n}\n:is(.pf2e) .markdown-rendered tr td:not(:last-child),\n:is(.pf2e) .markdown-rendered tr th:not(:last-child) {\n  border-right: 0.0625rem solid rgb(234, 229, 213);\n}\n\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n.callout[data-callout=pf2-beige] {\n  --callout-border-opacity: 0.3;\n  --callout-color: #f0e9d6;\n  --callout-background-alpha: 0.2;\n  --callout-padding: 1rem;\n  --callout-radius: 0.25rem;\n  font-size: 1rem;\n  overflow-wrap: break-word;\n  overflow-y: scroll;\n  background-color: #f0e9d6;\n  color: rgb(36, 36, 36);\n  border-color: var(--pf2e-light-brown-headers);\n  border-style: solid;\n  border-width: 0.05rem;\n  margin: 0.1875rem 0.125rem 0.125rem;\n  padding: 0.125rem;\n}\n.callout[data-callout=pf2-beige]:not(.admonition).drop-shadow {\n  box-shadow: rgb(0, 73, 97) 0 1.25rem 1.875rem -0.625rem;\n}\n.callout[data-callout=pf2-beige] .callout-title {\n  flex-direction: row;\n  flex-wrap: wrap;\n  justify-content: center;\n  align-items: center;\n  align-content: center;\n  border: none;\n  display: flex;\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  position: relative;\n}\n.callout[data-callout=pf2-beige] .callout-title .callout-icon .svg-icon {\n  display: none;\n}\n.callout[data-callout=pf2-beige] .callout-title .callout-title-inner {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  font-weight: 700;\n  margin-left: 1rem;\n  padding-bottom: 0.125rem;\n}\n.callout[data-callout=pf2-beige] .callout-title .callout-title-inner em {\n  font-weight: 500;\n}\n.callout[data-callout=pf2-beige] .callout-content {\n  background-color: #f0e9d6;\n  padding: 1rem;\n}\n.callout[data-callout=pf2-beige] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-beige] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-beige] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-beige] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=pf2-beige] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=pf2-beige] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=pf2-beige] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-beige] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=pf2-beige] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-beige] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-beige] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-beige] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=pf2-beige] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=pf2-beige] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=pf2-beige] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-beige] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=pf2-beige] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-beige] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-beige] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-beige] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=pf2-beige] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=pf2-beige] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=pf2-beige] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-beige] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=pf2-beige] .callout-content code {\n  background-color: rgb(232, 232, 232);\n  color: rgb(80, 62, 141);\n  font-family: var(--pf2e-font-monospace);\n}\n.callout[data-callout=pf2-beige] .callout-content a.internal-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.callout[data-callout=pf2-beige] .callout-content a.internal-link:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=pf2-beige] .callout-content a.internal-link.is-unresolved {\n  font-style: italic;\n  text-decoration: none;\n}\n.callout[data-callout=pf2-beige] .callout-content a.internal-link.is-unresolved:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=pf2-beige] .callout-content a.external-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.callout[data-callout=pf2-beige] .callout-content a.external-link:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=pf2-beige] .callout-content strong {\n  padding-inline: 0.25rem;\n}\n.callout[data-callout=pf2-beige] .callout-content blockquote {\n  padding: 0.3125rem 0.3125rem 0.3125rem 0.625rem;\n  border-right: 0.1875rem solid;\n  border-left: 0.1875rem solid;\n  margin-left: 0.75em;\n  margin-right: 0.75em;\n}\n.callout[data-callout=pf2-beige] .callout-content p {\n  line-height: 1.2;\n  margin-block-start: 0;\n  margin-block-end: 0;\n  margin-left: 0.5rem;\n  margin-bottom: 1rem;\n}\n\n.callout[data-callout=pf2-brown] {\n  --callout-border-opacity: 0.3;\n  --callout-color: #d2beaa;\n  --callout-background-alpha: 0.2;\n  --callout-padding: 1rem;\n  --callout-radius: 0.25rem;\n  font-size: 1rem;\n  overflow-wrap: break-word;\n  overflow-y: scroll;\n  background-color: #d2beaa;\n  color: rgb(36, 36, 36);\n  border-color: var(--pf2e-light-brown-headers);\n  border-style: solid;\n  border-width: 0.05rem;\n  margin: 0.1875rem 0.125rem 0.125rem;\n  padding: 0.125rem;\n}\n.callout[data-callout=pf2-brown]:not(.admonition).drop-shadow {\n  box-shadow: rgb(0, 73, 97) 0 1.25rem 1.875rem -0.625rem;\n}\n.callout[data-callout=pf2-brown] .callout-title {\n  flex-direction: row;\n  flex-wrap: wrap;\n  justify-content: center;\n  align-items: center;\n  align-content: center;\n  border: none;\n  display: flex;\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  position: relative;\n}\n.callout[data-callout=pf2-brown] .callout-title .callout-icon .svg-icon {\n  display: none;\n}\n.callout[data-callout=pf2-brown] .callout-title .callout-title-inner {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  font-weight: 700;\n  margin-left: 1rem;\n  padding-bottom: 0.125rem;\n}\n.callout[data-callout=pf2-brown] .callout-title .callout-title-inner em {\n  font-weight: 500;\n}\n.callout[data-callout=pf2-brown] .callout-content {\n  background-color: #d2beaa;\n  padding: 1rem;\n}\n.callout[data-callout=pf2-brown] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-brown] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-brown] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-brown] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=pf2-brown] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=pf2-brown] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=pf2-brown] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-brown] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=pf2-brown] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-brown] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-brown] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-brown] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=pf2-brown] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=pf2-brown] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=pf2-brown] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-brown] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=pf2-brown] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-brown] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-brown] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-brown] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=pf2-brown] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=pf2-brown] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=pf2-brown] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-brown] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=pf2-brown] .callout-content code {\n  background-color: rgb(232, 232, 232);\n  color: rgb(80, 62, 141);\n  font-family: var(--pf2e-font-monospace);\n}\n.callout[data-callout=pf2-brown] .callout-content a.internal-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.callout[data-callout=pf2-brown] .callout-content a.internal-link:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=pf2-brown] .callout-content a.internal-link.is-unresolved {\n  font-style: italic;\n  text-decoration: none;\n}\n.callout[data-callout=pf2-brown] .callout-content a.internal-link.is-unresolved:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=pf2-brown] .callout-content a.external-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.callout[data-callout=pf2-brown] .callout-content a.external-link:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=pf2-brown] .callout-content strong {\n  padding-inline: 0.25rem;\n}\n.callout[data-callout=pf2-brown] .callout-content blockquote {\n  padding: 0.3125rem 0.3125rem 0.3125rem 0.625rem;\n  border-right: 0.1875rem solid;\n  border-left: 0.1875rem solid;\n  margin-left: 0.75em;\n  margin-right: 0.75em;\n}\n.callout[data-callout=pf2-brown] .callout-content p {\n  line-height: 1.2;\n  margin-block-start: 0;\n  margin-block-end: 0;\n  margin-left: 0.5rem;\n  margin-bottom: 1rem;\n}\n\n.callout[data-callout=pf2-example] {\n  --callout-border-opacity: 0.3;\n  --callout-color: #ccdbbd;\n  --callout-background-alpha: 0.2;\n  --callout-padding: 1rem;\n  --callout-radius: 0.25rem;\n  font-size: 1rem;\n  overflow-wrap: break-word;\n  overflow-y: scroll;\n  background-color: #ccdbbd;\n  color: rgb(36, 36, 36);\n  border-color: var(--pf2e-light-brown-headers);\n  border-style: solid;\n  border-width: 0.05rem;\n  margin: 0.1875rem 0.125rem 0.125rem;\n  padding: 0.125rem;\n}\n.callout[data-callout=pf2-example]:not(.admonition).drop-shadow {\n  box-shadow: rgb(0, 73, 97) 0 1.25rem 1.875rem -0.625rem;\n}\n.callout[data-callout=pf2-example] .callout-title {\n  flex-direction: row;\n  flex-wrap: wrap;\n  justify-content: center;\n  align-items: center;\n  align-content: center;\n  border: none;\n  display: flex;\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  position: relative;\n}\n.callout[data-callout=pf2-example] .callout-title .callout-icon .svg-icon {\n  display: none;\n}\n.callout[data-callout=pf2-example] .callout-title .callout-title-inner {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  font-weight: 700;\n  margin-left: 1rem;\n  padding-bottom: 0.125rem;\n}\n.callout[data-callout=pf2-example] .callout-title .callout-title-inner em {\n  font-weight: 500;\n}\n.callout[data-callout=pf2-example] .callout-content {\n  background-color: #ccdbbd;\n  padding: 1rem;\n}\n.callout[data-callout=pf2-example] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-example] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-example] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-example] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=pf2-example] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=pf2-example] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=pf2-example] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-example] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=pf2-example] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-example] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-example] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-example] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=pf2-example] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=pf2-example] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=pf2-example] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-example] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=pf2-example] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-example] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-example] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-example] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=pf2-example] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=pf2-example] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=pf2-example] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-example] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=pf2-example] .callout-content code {\n  background-color: rgb(232, 232, 232);\n  color: rgb(80, 62, 141);\n  font-family: var(--pf2e-font-monospace);\n}\n.callout[data-callout=pf2-example] .callout-content a.internal-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.callout[data-callout=pf2-example] .callout-content a.internal-link:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=pf2-example] .callout-content a.internal-link.is-unresolved {\n  font-style: italic;\n  text-decoration: none;\n}\n.callout[data-callout=pf2-example] .callout-content a.internal-link.is-unresolved:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=pf2-example] .callout-content a.external-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.callout[data-callout=pf2-example] .callout-content a.external-link:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=pf2-example] .callout-content strong {\n  padding-inline: 0.25rem;\n}\n.callout[data-callout=pf2-example] .callout-content blockquote {\n  padding: 0.3125rem 0.3125rem 0.3125rem 0.625rem;\n  border-right: 0.1875rem solid;\n  border-left: 0.1875rem solid;\n  margin-left: 0.75em;\n  margin-right: 0.75em;\n}\n.callout[data-callout=pf2-example] .callout-content p {\n  line-height: 1.2;\n  margin-block-start: 0;\n  margin-block-end: 0;\n  margin-left: 0.5rem;\n  margin-bottom: 1rem;\n}\n\n.callout[data-callout=pf2-inset] {\n  --callout-border-opacity: 0.3;\n  --callout-color: #e9e4d9;\n  --callout-background-alpha: 0.2;\n  --callout-padding: 1rem;\n  --callout-radius: 0.25rem;\n  font-size: 1rem;\n  overflow-wrap: break-word;\n  overflow-y: scroll;\n  background-color: #e9e4d9;\n  color: rgb(36, 36, 36);\n  border-color: var(--pf2e-light-brown-headers);\n  border-style: solid;\n  border-width: 0.05rem;\n  margin: 0.1875rem 0.125rem 0.125rem;\n  padding: 0.125rem;\n}\n.callout[data-callout=pf2-inset]:not(.admonition).drop-shadow {\n  box-shadow: rgb(0, 73, 97) 0 1.25rem 1.875rem -0.625rem;\n}\n.callout[data-callout=pf2-inset] .callout-title {\n  flex-direction: row;\n  flex-wrap: wrap;\n  justify-content: center;\n  align-items: center;\n  align-content: center;\n  border: none;\n  display: flex;\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  position: relative;\n}\n.callout[data-callout=pf2-inset] .callout-title .callout-icon .svg-icon {\n  display: none;\n}\n.callout[data-callout=pf2-inset] .callout-title .callout-title-inner {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  font-weight: 700;\n  margin-left: 1rem;\n  padding-bottom: 0.125rem;\n}\n.callout[data-callout=pf2-inset] .callout-title .callout-title-inner em {\n  font-weight: 500;\n}\n.callout[data-callout=pf2-inset] .callout-content {\n  background-color: #e9e4d9;\n  padding: 1rem;\n}\n.callout[data-callout=pf2-inset] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-inset] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-inset] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-inset] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=pf2-inset] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=pf2-inset] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=pf2-inset] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-inset] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=pf2-inset] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-inset] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-inset] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-inset] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=pf2-inset] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=pf2-inset] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=pf2-inset] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-inset] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=pf2-inset] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-inset] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-inset] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-inset] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=pf2-inset] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=pf2-inset] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=pf2-inset] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-inset] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=pf2-inset] .callout-content code {\n  background-color: rgb(232, 232, 232);\n  color: rgb(80, 62, 141);\n  font-family: var(--pf2e-font-monospace);\n}\n.callout[data-callout=pf2-inset] .callout-content a.internal-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.callout[data-callout=pf2-inset] .callout-content a.internal-link:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=pf2-inset] .callout-content a.internal-link.is-unresolved {\n  font-style: italic;\n  text-decoration: none;\n}\n.callout[data-callout=pf2-inset] .callout-content a.internal-link.is-unresolved:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=pf2-inset] .callout-content a.external-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.callout[data-callout=pf2-inset] .callout-content a.external-link:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=pf2-inset] .callout-content strong {\n  padding-inline: 0.25rem;\n}\n.callout[data-callout=pf2-inset] .callout-content blockquote {\n  padding: 0.3125rem 0.3125rem 0.3125rem 0.625rem;\n  border-right: 0.1875rem solid;\n  border-left: 0.1875rem solid;\n  margin-left: 0.75em;\n  margin-right: 0.75em;\n}\n.callout[data-callout=pf2-inset] .callout-content p {\n  line-height: 1.2;\n  margin-block-start: 0;\n  margin-block-end: 0;\n  margin-left: 0.5rem;\n  margin-bottom: 1rem;\n}\n\n.callout[data-callout=pf2-key-box] {\n  --callout-border-opacity: 0.3;\n  --callout-color: #550c06;\n  --callout-background-alpha: 0.2;\n  --callout-padding: 1rem;\n  --callout-radius: 0.25rem;\n  font-size: 1rem;\n  overflow-wrap: break-word;\n  overflow-y: scroll;\n  background-color: #550c06;\n  color: rgb(36, 36, 36);\n  border-color: var(--pf2e-light-brown-headers);\n  border-style: solid;\n  border-width: 0.05rem;\n  margin: 0.1875rem 0.125rem 0.125rem;\n  padding: 0.125rem;\n}\n.callout[data-callout=pf2-key-box]:not(.admonition).drop-shadow {\n  box-shadow: rgb(0, 73, 97) 0 1.25rem 1.875rem -0.625rem;\n}\n.callout[data-callout=pf2-key-box] .callout-title {\n  flex-direction: row;\n  flex-wrap: wrap;\n  justify-content: center;\n  align-items: center;\n  align-content: center;\n  border: none;\n  display: flex;\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  position: relative;\n}\n.callout[data-callout=pf2-key-box] .callout-title .callout-icon .svg-icon {\n  display: none;\n}\n.callout[data-callout=pf2-key-box] .callout-title .callout-title-inner {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  font-weight: 700;\n  margin-left: 1rem;\n  padding-bottom: 0.125rem;\n}\n.callout[data-callout=pf2-key-box] .callout-title .callout-title-inner em {\n  font-weight: 500;\n}\n.callout[data-callout=pf2-key-box] .callout-content {\n  background-color: #550c06;\n  padding: 1rem;\n}\n.callout[data-callout=pf2-key-box] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-key-box] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-key-box] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-key-box] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=pf2-key-box] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=pf2-key-box] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=pf2-key-box] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-key-box] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=pf2-key-box] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-key-box] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-key-box] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-key-box] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=pf2-key-box] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=pf2-key-box] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=pf2-key-box] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-key-box] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=pf2-key-box] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-key-box] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-key-box] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-key-box] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=pf2-key-box] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=pf2-key-box] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=pf2-key-box] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-key-box] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=pf2-key-box] .callout-content code {\n  background-color: rgb(232, 232, 232);\n  color: rgb(80, 62, 141);\n  font-family: var(--pf2e-font-monospace);\n}\n.callout[data-callout=pf2-key-box] .callout-content a.internal-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.callout[data-callout=pf2-key-box] .callout-content a.internal-link:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=pf2-key-box] .callout-content a.internal-link.is-unresolved {\n  font-style: italic;\n  text-decoration: none;\n}\n.callout[data-callout=pf2-key-box] .callout-content a.internal-link.is-unresolved:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=pf2-key-box] .callout-content a.external-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.callout[data-callout=pf2-key-box] .callout-content a.external-link:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=pf2-key-box] .callout-content strong {\n  padding-inline: 0.25rem;\n}\n.callout[data-callout=pf2-key-box] .callout-content blockquote {\n  padding: 0.3125rem 0.3125rem 0.3125rem 0.625rem;\n  border-right: 0.1875rem solid;\n  border-left: 0.1875rem solid;\n  margin-left: 0.75em;\n  margin-right: 0.75em;\n}\n.callout[data-callout=pf2-key-box] .callout-content p {\n  line-height: 1.2;\n  margin-block-start: 0;\n  margin-block-end: 0;\n  margin-left: 0.5rem;\n  margin-bottom: 1rem;\n}\n\n.callout[data-callout=pf2-quote] {\n  --callout-border-opacity: 0.3;\n  --callout-color: #e7aad4;\n  --callout-background-alpha: 0.2;\n  --callout-padding: 1rem;\n  --callout-radius: 0.25rem;\n  font-size: 1rem;\n  overflow-wrap: break-word;\n  overflow-y: scroll;\n  background-color: #e7aad4;\n  color: rgb(36, 36, 36);\n  border-color: var(--pf2e-light-brown-headers);\n  border-style: solid;\n  border-width: 0.05rem;\n  margin: 0.1875rem 0.125rem 0.125rem;\n  padding: 0.125rem;\n}\n.callout[data-callout=pf2-quote]:not(.admonition).drop-shadow {\n  box-shadow: rgb(0, 73, 97) 0 1.25rem 1.875rem -0.625rem;\n}\n.callout[data-callout=pf2-quote] .callout-title {\n  flex-direction: row;\n  flex-wrap: wrap;\n  justify-content: center;\n  align-items: center;\n  align-content: center;\n  border: none;\n  display: flex;\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  position: relative;\n}\n.callout[data-callout=pf2-quote] .callout-title .callout-icon .svg-icon {\n  display: none;\n}\n.callout[data-callout=pf2-quote] .callout-title .callout-title-inner {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  font-weight: 700;\n  margin-left: 1rem;\n  padding-bottom: 0.125rem;\n}\n.callout[data-callout=pf2-quote] .callout-title .callout-title-inner em {\n  font-weight: 500;\n}\n.callout[data-callout=pf2-quote] .callout-content {\n  background-color: #e7aad4;\n  padding: 1rem;\n}\n.callout[data-callout=pf2-quote] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-quote] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-quote] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-quote] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=pf2-quote] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=pf2-quote] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=pf2-quote] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-quote] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=pf2-quote] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-quote] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-quote] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-quote] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=pf2-quote] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=pf2-quote] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=pf2-quote] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-quote] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=pf2-quote] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-quote] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-quote] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-quote] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=pf2-quote] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=pf2-quote] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=pf2-quote] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-quote] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=pf2-quote] .callout-content code {\n  background-color: rgb(232, 232, 232);\n  color: rgb(80, 62, 141);\n  font-family: var(--pf2e-font-monospace);\n}\n.callout[data-callout=pf2-quote] .callout-content a.internal-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.callout[data-callout=pf2-quote] .callout-content a.internal-link:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=pf2-quote] .callout-content a.internal-link.is-unresolved {\n  font-style: italic;\n  text-decoration: none;\n}\n.callout[data-callout=pf2-quote] .callout-content a.internal-link.is-unresolved:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=pf2-quote] .callout-content a.external-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.callout[data-callout=pf2-quote] .callout-content a.external-link:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=pf2-quote] .callout-content strong {\n  padding-inline: 0.25rem;\n}\n.callout[data-callout=pf2-quote] .callout-content blockquote {\n  padding: 0.3125rem 0.3125rem 0.3125rem 0.625rem;\n  border-right: 0.1875rem solid;\n  border-left: 0.1875rem solid;\n  margin-left: 0.75em;\n  margin-right: 0.75em;\n}\n.callout[data-callout=pf2-quote] .callout-content p {\n  line-height: 1.2;\n  margin-block-start: 0;\n  margin-block-end: 0;\n  margin-left: 0.5rem;\n  margin-bottom: 1rem;\n}\n\n.callout[data-callout=pf2-sidebar] {\n  --callout-border-opacity: 0.3;\n  --callout-color: #f4f3f0;\n  --callout-background-alpha: 0.2;\n  --callout-padding: 1rem;\n  --callout-radius: 0.25rem;\n  font-size: 1rem;\n  overflow-wrap: break-word;\n  overflow-y: scroll;\n  background-color: #f4f3f0;\n  color: rgb(36, 36, 36);\n  border-color: var(--pf2e-light-brown-headers);\n  border-style: solid;\n  border-width: 0.05rem;\n  margin: 0.1875rem 0.125rem 0.125rem;\n  padding: 0.125rem;\n}\n.callout[data-callout=pf2-sidebar]:not(.admonition).drop-shadow {\n  box-shadow: rgb(0, 73, 97) 0 1.25rem 1.875rem -0.625rem;\n}\n.callout[data-callout=pf2-sidebar] .callout-title {\n  flex-direction: row;\n  flex-wrap: wrap;\n  justify-content: center;\n  align-items: center;\n  align-content: center;\n  border: none;\n  display: flex;\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  position: relative;\n}\n.callout[data-callout=pf2-sidebar] .callout-title .callout-icon .svg-icon {\n  display: none;\n}\n.callout[data-callout=pf2-sidebar] .callout-title .callout-title-inner {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  font-weight: 700;\n  margin-left: 1rem;\n  padding-bottom: 0.125rem;\n}\n.callout[data-callout=pf2-sidebar] .callout-title .callout-title-inner em {\n  font-weight: 500;\n}\n.callout[data-callout=pf2-sidebar] .callout-content {\n  background-color: #f4f3f0;\n  padding: 1rem;\n}\n.callout[data-callout=pf2-sidebar] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-sidebar] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-sidebar] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-sidebar] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=pf2-sidebar] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=pf2-sidebar] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=pf2-sidebar] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-sidebar] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=pf2-sidebar] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-sidebar] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-sidebar] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-sidebar] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=pf2-sidebar] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=pf2-sidebar] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=pf2-sidebar] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-sidebar] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=pf2-sidebar] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-sidebar] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-sidebar] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-sidebar] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=pf2-sidebar] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=pf2-sidebar] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=pf2-sidebar] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-sidebar] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=pf2-sidebar] .callout-content code {\n  background-color: rgb(232, 232, 232);\n  color: rgb(80, 62, 141);\n  font-family: var(--pf2e-font-monospace);\n}\n.callout[data-callout=pf2-sidebar] .callout-content a.internal-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.callout[data-callout=pf2-sidebar] .callout-content a.internal-link:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=pf2-sidebar] .callout-content a.internal-link.is-unresolved {\n  font-style: italic;\n  text-decoration: none;\n}\n.callout[data-callout=pf2-sidebar] .callout-content a.internal-link.is-unresolved:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=pf2-sidebar] .callout-content a.external-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.callout[data-callout=pf2-sidebar] .callout-content a.external-link:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=pf2-sidebar] .callout-content strong {\n  padding-inline: 0.25rem;\n}\n.callout[data-callout=pf2-sidebar] .callout-content blockquote {\n  padding: 0.3125rem 0.3125rem 0.3125rem 0.625rem;\n  border-right: 0.1875rem solid;\n  border-left: 0.1875rem solid;\n  margin-left: 0.75em;\n  margin-right: 0.75em;\n}\n.callout[data-callout=pf2-sidebar] .callout-content p {\n  line-height: 1.2;\n  margin-block-start: 0;\n  margin-block-end: 0;\n  margin-left: 0.5rem;\n  margin-bottom: 1rem;\n}\n\n.callout[data-callout=pf2-red] {\n  --callout-border-opacity: 0.3;\n  --callout-color: #550c06;\n  --callout-background-alpha: 0.2;\n  --callout-padding: 1rem;\n  --callout-radius: 0.25rem;\n  font-size: 1rem;\n  overflow-wrap: break-word;\n  overflow-y: scroll;\n  background-color: #550c06;\n  color: rgb(36, 36, 36);\n  border-color: var(--pf2e-light-brown-headers);\n  border-style: solid;\n  border-width: 0.05rem;\n  margin: 0.1875rem 0.125rem 0.125rem;\n  padding: 0.125rem;\n}\n.callout[data-callout=pf2-red]:not(.admonition).drop-shadow {\n  box-shadow: rgb(0, 73, 97) 0 1.25rem 1.875rem -0.625rem;\n}\n.callout[data-callout=pf2-red] .callout-title {\n  flex-direction: row;\n  flex-wrap: wrap;\n  justify-content: center;\n  align-items: center;\n  align-content: center;\n  border: none;\n  display: flex;\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  position: relative;\n}\n.callout[data-callout=pf2-red] .callout-title .callout-icon .svg-icon {\n  display: none;\n}\n.callout[data-callout=pf2-red] .callout-title .callout-title-inner {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  font-weight: 700;\n  margin-left: 1rem;\n  padding-bottom: 0.125rem;\n}\n.callout[data-callout=pf2-red] .callout-title .callout-title-inner em {\n  font-weight: 500;\n}\n.callout[data-callout=pf2-red] .callout-content {\n  background-color: #550c06;\n  padding: 1rem;\n}\n.callout[data-callout=pf2-red] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-red] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-red] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-red] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=pf2-red] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=pf2-red] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=pf2-red] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-red] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=pf2-red] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-red] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-red] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-red] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=pf2-red] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=pf2-red] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=pf2-red] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-red] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=pf2-red] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-red] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-red] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-red] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=pf2-red] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=pf2-red] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=pf2-red] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-red] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=pf2-red] .callout-content code {\n  background-color: rgb(232, 232, 232);\n  color: rgb(80, 62, 141);\n  font-family: var(--pf2e-font-monospace);\n}\n.callout[data-callout=pf2-red] .callout-content a.internal-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.callout[data-callout=pf2-red] .callout-content a.internal-link:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=pf2-red] .callout-content a.internal-link.is-unresolved {\n  font-style: italic;\n  text-decoration: none;\n}\n.callout[data-callout=pf2-red] .callout-content a.internal-link.is-unresolved:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=pf2-red] .callout-content a.external-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.callout[data-callout=pf2-red] .callout-content a.external-link:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=pf2-red] .callout-content strong {\n  padding-inline: 0.25rem;\n}\n.callout[data-callout=pf2-red] .callout-content blockquote {\n  padding: 0.3125rem 0.3125rem 0.3125rem 0.625rem;\n  border-right: 0.1875rem solid;\n  border-left: 0.1875rem solid;\n  margin-left: 0.75em;\n  margin-right: 0.75em;\n}\n.callout[data-callout=pf2-red] .callout-content p {\n  line-height: 1.2;\n  margin-block-start: 0;\n  margin-block-end: 0;\n  margin-left: 0.5rem;\n  margin-bottom: 1rem;\n}\n\n.callout[data-callout=pf2-tip] {\n  --callout-border-opacity: 0.3;\n  --callout-color: #d1d3d4;\n  --callout-background-alpha: 0.2;\n  --callout-padding: 1rem;\n  --callout-radius: 0.25rem;\n  font-size: 1rem;\n  overflow-wrap: break-word;\n  overflow-y: scroll;\n  background-color: #d1d3d4;\n  color: rgb(36, 36, 36);\n  border-color: var(--pf2e-light-brown-headers);\n  border-style: solid;\n  border-width: 0.05rem;\n  margin: 0.1875rem 0.125rem 0.125rem;\n  padding: 0.125rem;\n}\n.callout[data-callout=pf2-tip]:not(.admonition).drop-shadow {\n  box-shadow: rgb(0, 73, 97) 0 1.25rem 1.875rem -0.625rem;\n}\n.callout[data-callout=pf2-tip] .callout-title {\n  flex-direction: row;\n  flex-wrap: wrap;\n  justify-content: center;\n  align-items: center;\n  align-content: center;\n  border: none;\n  display: flex;\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  position: relative;\n}\n.callout[data-callout=pf2-tip] .callout-title .callout-icon .svg-icon {\n  display: none;\n}\n.callout[data-callout=pf2-tip] .callout-title .callout-title-inner {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  font-weight: 700;\n  margin-left: 1rem;\n  padding-bottom: 0.125rem;\n}\n.callout[data-callout=pf2-tip] .callout-title .callout-title-inner em {\n  font-weight: 500;\n}\n.callout[data-callout=pf2-tip] .callout-content {\n  background-color: #d1d3d4;\n  padding: 1rem;\n}\n.callout[data-callout=pf2-tip] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-tip] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-tip] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-tip] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=pf2-tip] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=pf2-tip] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=pf2-tip] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-tip] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=pf2-tip] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-tip] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-tip] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-tip] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=pf2-tip] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=pf2-tip] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=pf2-tip] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-tip] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=pf2-tip] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-tip] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-tip] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-tip] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=pf2-tip] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=pf2-tip] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=pf2-tip] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-tip] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=pf2-tip] .callout-content code {\n  background-color: rgb(232, 232, 232);\n  color: rgb(80, 62, 141);\n  font-family: var(--pf2e-font-monospace);\n}\n.callout[data-callout=pf2-tip] .callout-content a.internal-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.callout[data-callout=pf2-tip] .callout-content a.internal-link:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=pf2-tip] .callout-content a.internal-link.is-unresolved {\n  font-style: italic;\n  text-decoration: none;\n}\n.callout[data-callout=pf2-tip] .callout-content a.internal-link.is-unresolved:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=pf2-tip] .callout-content a.external-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.callout[data-callout=pf2-tip] .callout-content a.external-link:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=pf2-tip] .callout-content strong {\n  padding-inline: 0.25rem;\n}\n.callout[data-callout=pf2-tip] .callout-content blockquote {\n  padding: 0.3125rem 0.3125rem 0.3125rem 0.625rem;\n  border-right: 0.1875rem solid;\n  border-left: 0.1875rem solid;\n  margin-left: 0.75em;\n  margin-right: 0.75em;\n}\n.callout[data-callout=pf2-tip] .callout-content p {\n  line-height: 1.2;\n  margin-block-start: 0;\n  margin-block-end: 0;\n  margin-left: 0.5rem;\n  margin-bottom: 1rem;\n}\n\n.callout[data-callout=pf2-note] {\n  --callout-border-opacity: 0.3;\n  --callout-color: #e3dfbb;\n  --callout-background-alpha: 0.2;\n  --callout-padding: 1rem;\n  --callout-radius: 0.25rem;\n  font-size: 1rem;\n  overflow-wrap: break-word;\n  overflow-y: scroll;\n  background-color: #e3dfbb;\n  color: rgb(36, 36, 36);\n  border-color: var(--pf2e-light-brown-headers);\n  border-style: solid;\n  border-width: 0.05rem;\n  margin: 0.1875rem 0.125rem 0.125rem;\n  padding: 0.125rem;\n}\n.callout[data-callout=pf2-note]:not(.admonition).drop-shadow {\n  box-shadow: rgb(0, 73, 97) 0 1.25rem 1.875rem -0.625rem;\n}\n.callout[data-callout=pf2-note] .callout-title {\n  flex-direction: row;\n  flex-wrap: wrap;\n  justify-content: center;\n  align-items: center;\n  align-content: center;\n  border: none;\n  display: flex;\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  position: relative;\n}\n.callout[data-callout=pf2-note] .callout-title .callout-icon .svg-icon {\n  display: none;\n}\n.callout[data-callout=pf2-note] .callout-title .callout-title-inner {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  font-weight: 700;\n  margin-left: 1rem;\n  padding-bottom: 0.125rem;\n}\n.callout[data-callout=pf2-note] .callout-title .callout-title-inner em {\n  font-weight: 500;\n}\n.callout[data-callout=pf2-note] .callout-content {\n  background-color: #e3dfbb;\n  padding: 1rem;\n}\n.callout[data-callout=pf2-note] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-note] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-note] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-note] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=pf2-note] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=pf2-note] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=pf2-note] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-note] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=pf2-note] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-note] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-note] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-note] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=pf2-note] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=pf2-note] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=pf2-note] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-note] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=pf2-note] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-note] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-note] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=pf2-note] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=pf2-note] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=pf2-note] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=pf2-note] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=pf2-note] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=pf2-note] .callout-content code {\n  background-color: rgb(232, 232, 232);\n  color: rgb(80, 62, 141);\n  font-family: var(--pf2e-font-monospace);\n}\n.callout[data-callout=pf2-note] .callout-content a.internal-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.callout[data-callout=pf2-note] .callout-content a.internal-link:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=pf2-note] .callout-content a.internal-link.is-unresolved {\n  font-style: italic;\n  text-decoration: none;\n}\n.callout[data-callout=pf2-note] .callout-content a.internal-link.is-unresolved:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=pf2-note] .callout-content a.external-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.callout[data-callout=pf2-note] .callout-content a.external-link:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=pf2-note] .callout-content strong {\n  padding-inline: 0.25rem;\n}\n.callout[data-callout=pf2-note] .callout-content blockquote {\n  padding: 0.3125rem 0.3125rem 0.3125rem 0.625rem;\n  border-right: 0.1875rem solid;\n  border-left: 0.1875rem solid;\n  margin-left: 0.75em;\n  margin-right: 0.75em;\n}\n.callout[data-callout=pf2-note] .callout-content p {\n  line-height: 1.2;\n  margin-block-start: 0;\n  margin-block-end: 0;\n  margin-left: 0.5rem;\n  margin-bottom: 1rem;\n}\n\n.callout[data-callout=success-degree] {\n  --callout-border-opacity: 0.3;\n  --callout-color: #e6e6b4;\n  --callout-background-alpha: 0.2;\n  --callout-padding: 1rem;\n  --callout-radius: 0.25rem;\n  font-size: 1rem;\n  overflow-wrap: break-word;\n  overflow-y: scroll;\n  background-color: #e6e6b4;\n  color: rgb(36, 36, 36);\n  border-color: var(--pf2e-light-brown-headers);\n  border-style: solid;\n  border-width: 0.05rem;\n  margin: 0.1875rem 0.125rem 0.125rem;\n  padding: 0.125rem;\n}\n.callout[data-callout=success-degree]:not(.admonition).drop-shadow {\n  box-shadow: rgb(0, 73, 97) 0 1.25rem 1.875rem -0.625rem;\n}\n.callout[data-callout=success-degree] .callout-title {\n  flex-direction: row;\n  flex-wrap: wrap;\n  justify-content: center;\n  align-items: center;\n  align-content: center;\n  border: none;\n  display: flex;\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  position: relative;\n}\n.callout[data-callout=success-degree] .callout-title .callout-icon .svg-icon {\n  display: none;\n}\n.callout[data-callout=success-degree] .callout-title .callout-title-inner {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  font-weight: 700;\n  margin-left: 1rem;\n  padding-bottom: 0.125rem;\n}\n.callout[data-callout=success-degree] .callout-title .callout-title-inner em {\n  font-weight: 500;\n}\n.callout[data-callout=success-degree] .callout-content {\n  background-color: #e6e6b4;\n  padding: 1rem;\n}\n.callout[data-callout=success-degree] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=success-degree] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=success-degree] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=success-degree] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=success-degree] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=success-degree] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=success-degree] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=success-degree] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=success-degree] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=success-degree] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=success-degree] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=success-degree] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=success-degree] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=success-degree] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=success-degree] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=success-degree] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=success-degree] .callout-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=success-degree] .callout-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=success-degree] .callout-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.callout[data-callout=success-degree] .callout-content h3::before {\n  display: none;\n}\n.callout[data-callout=success-degree] .callout-content h3::after {\n  display: none;\n}\n.callout[data-callout=success-degree] .callout-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.callout[data-callout=success-degree] .callout-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.callout[data-callout=success-degree] .callout-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.callout[data-callout=success-degree] .callout-content code {\n  background-color: rgb(232, 232, 232);\n  color: rgb(80, 62, 141);\n  font-family: var(--pf2e-font-monospace);\n}\n.callout[data-callout=success-degree] .callout-content a.internal-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.callout[data-callout=success-degree] .callout-content a.internal-link:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=success-degree] .callout-content a.internal-link.is-unresolved {\n  font-style: italic;\n  text-decoration: none;\n}\n.callout[data-callout=success-degree] .callout-content a.internal-link.is-unresolved:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=success-degree] .callout-content a.external-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.callout[data-callout=success-degree] .callout-content a.external-link:hover {\n  text-decoration: underline;\n}\n.callout[data-callout=success-degree] .callout-content strong {\n  padding-inline: 0.25rem;\n}\n.callout[data-callout=success-degree] .callout-content blockquote {\n  padding: 0.3125rem 0.3125rem 0.3125rem 0.625rem;\n  border-right: 0.1875rem solid;\n  border-left: 0.1875rem solid;\n  margin-left: 0.75em;\n  margin-right: 0.75em;\n}\n.callout[data-callout=success-degree] .callout-content p {\n  line-height: 1.2;\n  margin-block-start: 0;\n  margin-block-end: 0;\n  margin-left: 0.5rem;\n  margin-bottom: 1rem;\n}\n\n.admonition[data-callout=pf2-note] {\n  --callout-border-opacity: 0.3;\n  --callout-color: rgb(227, 223, 187);\n  --callout-content-background: rgb(227, 223, 187);\n  --callout-background-alpha: 0.2;\n  --callout-padding: 1rem;\n  --callout-radius: 0.25rem;\n  font-size: 1rem;\n  overflow-wrap: break-word;\n  overflow-y: scroll;\n  box-shadow: 0 0 5px 0 rgba(26, 34, 77, 0.9);\n  border-color: var(--pf2e-light-brown-headers);\n  border-style: solid;\n  border-width: 0.05rem;\n  color: rgb(36, 36, 36);\n  margin: 3px 0.125rem 0.125rem;\n  padding: 0.125rem;\n}\n.admonition[data-callout=pf2-note] .admonition-title {\n  background-color: rgb(227, 223, 187);\n  flex-direction: row;\n  flex-wrap: wrap;\n  justify-content: center;\n  align-items: center;\n  align-content: center;\n  border: none;\n  display: flex;\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  position: relative;\n}\n.admonition[data-callout=pf2-note] .admonition-title .admonition-title-icon .svg-icon {\n  display: block;\n  height: 1.5rem;\n  left: 0.5rem;\n  margin-left: 0.5rem;\n  position: absolute;\n  top: 0.5rem;\n  width: 1.5rem;\n}\n.admonition[data-callout=pf2-note] .admonition-title .admonition-title-icon .svg-icon:has(.content) {\n  height: 1rem;\n  width: 1rem;\n}\n.admonition[data-callout=pf2-note] .admonition-title .admonition-title-inner {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  font-weight: 700;\n  padding-bottom: 0.125rem;\n  margin-right: 0.125rem;\n}\n.admonition[data-callout=pf2-note] .admonition-title .admonition-title-inner em {\n  font-weight: 500;\n}\n.admonition[data-callout=pf2-note] .admonition-content {\n  background-color: rgb(227, 223, 187);\n  padding: 1rem;\n}\n.admonition[data-callout=pf2-note] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-note] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-note] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-note] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=pf2-note] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=pf2-note] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-note] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-note] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-note] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-note] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-note] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-note] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=pf2-note] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=pf2-note] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-note] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-note] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-note] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-note] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-note] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-note] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=pf2-note] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=pf2-note] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-note] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-note] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-note] .admonition-content code {\n  background-color: rgb(232, 232, 232);\n  color: rgb(80, 62, 141);\n  font-family: var(--pf2e-font-monospace);\n}\n.admonition[data-callout=pf2-note] .admonition-content a.internal-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.admonition[data-callout=pf2-note] .admonition-content a.internal-link:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=pf2-note] .admonition-content a.internal-link.is-unresolved {\n  font-style: italic;\n  text-decoration: none;\n}\n.admonition[data-callout=pf2-note] .admonition-content a.internal-link.is-unresolved:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=pf2-note] .admonition-content a.external-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.admonition[data-callout=pf2-note] .admonition-content a.external-link:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=pf2-note] .admonition-content strong {\n  padding-inline: 0.25rem;\n}\n.admonition[data-callout=pf2-note] .admonition-content blockquote {\n  padding: 0.3125rem 0.3125rem 0.3125rem 0.625rem;\n  border-right: 0.1875rem solid;\n  border-left: 0.1875rem solid;\n  margin-left: 0.75em;\n  margin-right: 0.75em;\n}\n.admonition[data-callout=pf2-note] .admonition-content p {\n  line-height: 1.2;\n  margin-block-start: 0;\n  margin-block-end: 0;\n  margin-left: 0.5rem;\n  margin-bottom: 1rem;\n}\n.admonition[data-callout=pf2-note] .admonition-content > .callout {\n  mix-blend-mode: normal;\n  margin-bottom: 1rem;\n}\n.admonition[data-callout=pf2-note] .admonition-content-copy {\n  opacity: 0;\n  margin: 0.3125rem;\n  right: 0;\n  top: 0;\n  transition: 1s opacity ease-in-out;\n}\n.admonition[data-callout=pf2-note] .admonition-content-copy:hover {\n  color: rgba(232, 232, 232, 0.3);\n}\n\n.admonition[data-callout=embed-ability] {\n  --callout-border-opacity: 0.3;\n  --callout-color: rgb(182, 197, 200);\n  --callout-content-background: rgb(182, 197, 200);\n  --callout-background-alpha: 0.2;\n  --callout-padding: 1rem;\n  --callout-radius: 0.25rem;\n  font-size: 1rem;\n  overflow-wrap: break-word;\n  overflow-y: scroll;\n  box-shadow: 0 0 5px 0 rgba(26, 34, 77, 0.9);\n  border-color: var(--pf2e-light-brown-headers);\n  border-style: solid;\n  border-width: 0.05rem;\n  color: rgb(36, 36, 36);\n  margin: 3px 0.125rem 0.125rem;\n  padding: 0.125rem;\n}\n.admonition[data-callout=embed-ability] .admonition-title {\n  background-color: rgb(182, 197, 200);\n  flex-direction: row;\n  flex-wrap: wrap;\n  justify-content: center;\n  align-items: center;\n  align-content: center;\n  border: none;\n  display: flex;\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  position: relative;\n}\n.admonition[data-callout=embed-ability] .admonition-title .admonition-title-icon .svg-icon {\n  display: block;\n  height: 1.5rem;\n  left: 0.5rem;\n  margin-left: 0.5rem;\n  position: absolute;\n  top: 0.5rem;\n  width: 1.5rem;\n}\n.admonition[data-callout=embed-ability] .admonition-title .admonition-title-icon .svg-icon:has(.content) {\n  height: 1rem;\n  width: 1rem;\n}\n.admonition[data-callout=embed-ability] .admonition-title .admonition-title-inner {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  font-weight: 700;\n  padding-bottom: 0.125rem;\n  margin-right: 0.125rem;\n}\n.admonition[data-callout=embed-ability] .admonition-title .admonition-title-inner em {\n  font-weight: 500;\n}\n.admonition[data-callout=embed-ability] .admonition-content {\n  background-color: rgb(182, 197, 200);\n  padding: 1rem;\n}\n.admonition[data-callout=embed-ability] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ability] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ability] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ability] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=embed-ability] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=embed-ability] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ability] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ability] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ability] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ability] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ability] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ability] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=embed-ability] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=embed-ability] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ability] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ability] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ability] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ability] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ability] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ability] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=embed-ability] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=embed-ability] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ability] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ability] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ability] .admonition-content code {\n  background-color: rgb(232, 232, 232);\n  color: rgb(80, 62, 141);\n  font-family: var(--pf2e-font-monospace);\n}\n.admonition[data-callout=embed-ability] .admonition-content a.internal-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.admonition[data-callout=embed-ability] .admonition-content a.internal-link:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=embed-ability] .admonition-content a.internal-link.is-unresolved {\n  font-style: italic;\n  text-decoration: none;\n}\n.admonition[data-callout=embed-ability] .admonition-content a.internal-link.is-unresolved:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=embed-ability] .admonition-content a.external-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.admonition[data-callout=embed-ability] .admonition-content a.external-link:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=embed-ability] .admonition-content strong {\n  padding-inline: 0.25rem;\n}\n.admonition[data-callout=embed-ability] .admonition-content blockquote {\n  padding: 0.3125rem 0.3125rem 0.3125rem 0.625rem;\n  border-right: 0.1875rem solid;\n  border-left: 0.1875rem solid;\n  margin-left: 0.75em;\n  margin-right: 0.75em;\n}\n.admonition[data-callout=embed-ability] .admonition-content p {\n  line-height: 1.2;\n  margin-block-start: 0;\n  margin-block-end: 0;\n  margin-left: 0.5rem;\n  margin-bottom: 1rem;\n}\n.admonition[data-callout=embed-ability] .admonition-content > .callout {\n  mix-blend-mode: normal;\n  margin-bottom: 1rem;\n}\n.admonition[data-callout=embed-ability] .admonition-content-copy {\n  opacity: 0;\n  margin: 0.3125rem;\n  right: 0;\n  top: 0;\n  transition: 1s opacity ease-in-out;\n}\n.admonition[data-callout=embed-ability] .admonition-content-copy:hover {\n  color: rgba(232, 232, 232, 0.3);\n}\n\n.admonition[data-callout=embed-action] {\n  --callout-border-opacity: 0.3;\n  --callout-color: rgb(238, 221, 227);\n  --callout-content-background: rgb(238, 221, 227);\n  --callout-background-alpha: 0.2;\n  --callout-padding: 1rem;\n  --callout-radius: 0.25rem;\n  font-size: 1rem;\n  overflow-wrap: break-word;\n  overflow-y: scroll;\n  box-shadow: 0 0 5px 0 rgba(26, 34, 77, 0.9);\n  border-color: var(--pf2e-light-brown-headers);\n  border-style: solid;\n  border-width: 0.05rem;\n  color: rgb(36, 36, 36);\n  margin: 3px 0.125rem 0.125rem;\n  padding: 0.125rem;\n}\n.admonition[data-callout=embed-action] .admonition-title {\n  background-color: rgb(238, 221, 227);\n  flex-direction: row;\n  flex-wrap: wrap;\n  justify-content: center;\n  align-items: center;\n  align-content: center;\n  border: none;\n  display: flex;\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  position: relative;\n}\n.admonition[data-callout=embed-action] .admonition-title .admonition-title-icon .svg-icon {\n  display: block;\n  height: 1.5rem;\n  left: 0.5rem;\n  margin-left: 0.5rem;\n  position: absolute;\n  top: 0.5rem;\n  width: 1.5rem;\n}\n.admonition[data-callout=embed-action] .admonition-title .admonition-title-icon .svg-icon:has(.content) {\n  height: 1rem;\n  width: 1rem;\n}\n.admonition[data-callout=embed-action] .admonition-title .admonition-title-inner {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  font-weight: 700;\n  padding-bottom: 0.125rem;\n  margin-right: 0.125rem;\n}\n.admonition[data-callout=embed-action] .admonition-title .admonition-title-inner em {\n  font-weight: 500;\n}\n.admonition[data-callout=embed-action] .admonition-content {\n  background-color: rgb(238, 221, 227);\n  padding: 1rem;\n}\n.admonition[data-callout=embed-action] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-action] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-action] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-action] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=embed-action] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=embed-action] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=embed-action] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-action] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=embed-action] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-action] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-action] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-action] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=embed-action] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=embed-action] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=embed-action] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-action] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=embed-action] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-action] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-action] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-action] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=embed-action] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=embed-action] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=embed-action] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-action] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=embed-action] .admonition-content code {\n  background-color: rgb(232, 232, 232);\n  color: rgb(80, 62, 141);\n  font-family: var(--pf2e-font-monospace);\n}\n.admonition[data-callout=embed-action] .admonition-content a.internal-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.admonition[data-callout=embed-action] .admonition-content a.internal-link:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=embed-action] .admonition-content a.internal-link.is-unresolved {\n  font-style: italic;\n  text-decoration: none;\n}\n.admonition[data-callout=embed-action] .admonition-content a.internal-link.is-unresolved:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=embed-action] .admonition-content a.external-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.admonition[data-callout=embed-action] .admonition-content a.external-link:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=embed-action] .admonition-content strong {\n  padding-inline: 0.25rem;\n}\n.admonition[data-callout=embed-action] .admonition-content blockquote {\n  padding: 0.3125rem 0.3125rem 0.3125rem 0.625rem;\n  border-right: 0.1875rem solid;\n  border-left: 0.1875rem solid;\n  margin-left: 0.75em;\n  margin-right: 0.75em;\n}\n.admonition[data-callout=embed-action] .admonition-content p {\n  line-height: 1.2;\n  margin-block-start: 0;\n  margin-block-end: 0;\n  margin-left: 0.5rem;\n  margin-bottom: 1rem;\n}\n.admonition[data-callout=embed-action] .admonition-content > .callout {\n  mix-blend-mode: normal;\n  margin-bottom: 1rem;\n}\n.admonition[data-callout=embed-action] .admonition-content-copy {\n  opacity: 0;\n  margin: 0.3125rem;\n  right: 0;\n  top: 0;\n  transition: 1s opacity ease-in-out;\n}\n.admonition[data-callout=embed-action] .admonition-content-copy:hover {\n  color: rgba(232, 232, 232, 0.3);\n}\n\n.admonition[data-callout=embed-avatar] {\n  --callout-border-opacity: 0.3;\n  --callout-color: rgb(236, 202, 99);\n  --callout-content-background: rgb(236, 202, 99);\n  --callout-background-alpha: 0.2;\n  --callout-padding: 1rem;\n  --callout-radius: 0.25rem;\n  font-size: 1rem;\n  overflow-wrap: break-word;\n  overflow-y: scroll;\n  box-shadow: 0 0 5px 0 rgba(26, 34, 77, 0.9);\n  border-color: var(--pf2e-light-brown-headers);\n  border-style: solid;\n  border-width: 0.05rem;\n  color: rgb(36, 36, 36);\n  margin: 3px 0.125rem 0.125rem;\n  padding: 0.125rem;\n}\n.admonition[data-callout=embed-avatar] .admonition-title {\n  background-color: rgb(236, 202, 99);\n  flex-direction: row;\n  flex-wrap: wrap;\n  justify-content: center;\n  align-items: center;\n  align-content: center;\n  border: none;\n  display: flex;\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  position: relative;\n}\n.admonition[data-callout=embed-avatar] .admonition-title .admonition-title-icon .svg-icon {\n  display: block;\n  height: 1.5rem;\n  left: 0.5rem;\n  margin-left: 0.5rem;\n  position: absolute;\n  top: 0.5rem;\n  width: 1.5rem;\n}\n.admonition[data-callout=embed-avatar] .admonition-title .admonition-title-icon .svg-icon:has(.content) {\n  height: 1rem;\n  width: 1rem;\n}\n.admonition[data-callout=embed-avatar] .admonition-title .admonition-title-inner {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  font-weight: 700;\n  padding-bottom: 0.125rem;\n  margin-right: 0.125rem;\n}\n.admonition[data-callout=embed-avatar] .admonition-title .admonition-title-inner em {\n  font-weight: 500;\n}\n.admonition[data-callout=embed-avatar] .admonition-content {\n  background-color: rgb(236, 202, 99);\n  padding: 1rem;\n}\n.admonition[data-callout=embed-avatar] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-avatar] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-avatar] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-avatar] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=embed-avatar] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=embed-avatar] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=embed-avatar] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-avatar] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=embed-avatar] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-avatar] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-avatar] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-avatar] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=embed-avatar] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=embed-avatar] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=embed-avatar] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-avatar] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=embed-avatar] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-avatar] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-avatar] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-avatar] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=embed-avatar] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=embed-avatar] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=embed-avatar] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-avatar] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=embed-avatar] .admonition-content code {\n  background-color: rgb(232, 232, 232);\n  color: rgb(80, 62, 141);\n  font-family: var(--pf2e-font-monospace);\n}\n.admonition[data-callout=embed-avatar] .admonition-content a.internal-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.admonition[data-callout=embed-avatar] .admonition-content a.internal-link:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=embed-avatar] .admonition-content a.internal-link.is-unresolved {\n  font-style: italic;\n  text-decoration: none;\n}\n.admonition[data-callout=embed-avatar] .admonition-content a.internal-link.is-unresolved:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=embed-avatar] .admonition-content a.external-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.admonition[data-callout=embed-avatar] .admonition-content a.external-link:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=embed-avatar] .admonition-content strong {\n  padding-inline: 0.25rem;\n}\n.admonition[data-callout=embed-avatar] .admonition-content blockquote {\n  padding: 0.3125rem 0.3125rem 0.3125rem 0.625rem;\n  border-right: 0.1875rem solid;\n  border-left: 0.1875rem solid;\n  margin-left: 0.75em;\n  margin-right: 0.75em;\n}\n.admonition[data-callout=embed-avatar] .admonition-content p {\n  line-height: 1.2;\n  margin-block-start: 0;\n  margin-block-end: 0;\n  margin-left: 0.5rem;\n  margin-bottom: 1rem;\n}\n.admonition[data-callout=embed-avatar] .admonition-content > .callout {\n  mix-blend-mode: normal;\n  margin-bottom: 1rem;\n}\n.admonition[data-callout=embed-avatar] .admonition-content-copy {\n  opacity: 0;\n  margin: 0.3125rem;\n  right: 0;\n  top: 0;\n  transition: 1s opacity ease-in-out;\n}\n.admonition[data-callout=embed-avatar] .admonition-content-copy:hover {\n  color: rgba(232, 232, 232, 0.3);\n}\n\n.admonition[data-callout=embed-disease] {\n  --callout-border-opacity: 0.3;\n  --callout-color: rgb(36, 36, 36);\n  --callout-content-background: rgb(36, 36, 36);\n  --callout-background-alpha: 0.2;\n  --callout-padding: 1rem;\n  --callout-radius: 0.25rem;\n  font-size: 1rem;\n  overflow-wrap: break-word;\n  overflow-y: scroll;\n  box-shadow: 0 0 5px 0 rgba(26, 34, 77, 0.9);\n  border-color: var(--pf2e-light-brown-headers);\n  border-style: solid;\n  border-width: 0.05rem;\n  color: rgb(36, 36, 36);\n  margin: 3px 0.125rem 0.125rem;\n  padding: 0.125rem;\n}\n.admonition[data-callout=embed-disease] .admonition-title {\n  background-color: rgb(36, 36, 36);\n  flex-direction: row;\n  flex-wrap: wrap;\n  justify-content: center;\n  align-items: center;\n  align-content: center;\n  border: none;\n  display: flex;\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  position: relative;\n}\n.admonition[data-callout=embed-disease] .admonition-title .admonition-title-icon .svg-icon {\n  display: block;\n  height: 1.5rem;\n  left: 0.5rem;\n  margin-left: 0.5rem;\n  position: absolute;\n  top: 0.5rem;\n  width: 1.5rem;\n}\n.admonition[data-callout=embed-disease] .admonition-title .admonition-title-icon .svg-icon:has(.content) {\n  height: 1rem;\n  width: 1rem;\n}\n.admonition[data-callout=embed-disease] .admonition-title .admonition-title-inner {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  font-weight: 700;\n  padding-bottom: 0.125rem;\n  margin-right: 0.125rem;\n}\n.admonition[data-callout=embed-disease] .admonition-title .admonition-title-inner em {\n  font-weight: 500;\n}\n.admonition[data-callout=embed-disease] .admonition-content {\n  background-color: rgb(36, 36, 36);\n  padding: 1rem;\n}\n.admonition[data-callout=embed-disease] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-disease] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-disease] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-disease] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=embed-disease] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=embed-disease] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=embed-disease] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-disease] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=embed-disease] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-disease] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-disease] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-disease] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=embed-disease] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=embed-disease] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=embed-disease] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-disease] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=embed-disease] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-disease] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-disease] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-disease] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=embed-disease] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=embed-disease] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=embed-disease] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-disease] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=embed-disease] .admonition-content code {\n  background-color: rgb(232, 232, 232);\n  color: rgb(80, 62, 141);\n  font-family: var(--pf2e-font-monospace);\n}\n.admonition[data-callout=embed-disease] .admonition-content a.internal-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.admonition[data-callout=embed-disease] .admonition-content a.internal-link:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=embed-disease] .admonition-content a.internal-link.is-unresolved {\n  font-style: italic;\n  text-decoration: none;\n}\n.admonition[data-callout=embed-disease] .admonition-content a.internal-link.is-unresolved:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=embed-disease] .admonition-content a.external-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.admonition[data-callout=embed-disease] .admonition-content a.external-link:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=embed-disease] .admonition-content strong {\n  padding-inline: 0.25rem;\n}\n.admonition[data-callout=embed-disease] .admonition-content blockquote {\n  padding: 0.3125rem 0.3125rem 0.3125rem 0.625rem;\n  border-right: 0.1875rem solid;\n  border-left: 0.1875rem solid;\n  margin-left: 0.75em;\n  margin-right: 0.75em;\n}\n.admonition[data-callout=embed-disease] .admonition-content p {\n  line-height: 1.2;\n  margin-block-start: 0;\n  margin-block-end: 0;\n  margin-left: 0.5rem;\n  margin-bottom: 1rem;\n}\n.admonition[data-callout=embed-disease] .admonition-content > .callout {\n  mix-blend-mode: normal;\n  margin-bottom: 1rem;\n}\n.admonition[data-callout=embed-disease] .admonition-content-copy {\n  opacity: 0;\n  margin: 0.3125rem;\n  right: 0;\n  top: 0;\n  transition: 1s opacity ease-in-out;\n}\n.admonition[data-callout=embed-disease] .admonition-content-copy:hover {\n  color: rgba(232, 232, 232, 0.3);\n}\n\n.admonition[data-callout=embed-feat] {\n  --callout-border-opacity: 0.3;\n  --callout-color: rgb(239, 187, 169);\n  --callout-content-background: rgb(239, 187, 169);\n  --callout-background-alpha: 0.2;\n  --callout-padding: 1rem;\n  --callout-radius: 0.25rem;\n  font-size: 1rem;\n  overflow-wrap: break-word;\n  overflow-y: scroll;\n  box-shadow: 0 0 5px 0 rgba(26, 34, 77, 0.9);\n  border-color: var(--pf2e-light-brown-headers);\n  border-style: solid;\n  border-width: 0.05rem;\n  color: rgb(36, 36, 36);\n  margin: 3px 0.125rem 0.125rem;\n  padding: 0.125rem;\n}\n.admonition[data-callout=embed-feat] .admonition-title {\n  background-color: rgb(239, 187, 169);\n  flex-direction: row;\n  flex-wrap: wrap;\n  justify-content: center;\n  align-items: center;\n  align-content: center;\n  border: none;\n  display: flex;\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  position: relative;\n}\n.admonition[data-callout=embed-feat] .admonition-title .admonition-title-icon .svg-icon {\n  display: block;\n  height: 1.5rem;\n  left: 0.5rem;\n  margin-left: 0.5rem;\n  position: absolute;\n  top: 0.5rem;\n  width: 1.5rem;\n}\n.admonition[data-callout=embed-feat] .admonition-title .admonition-title-icon .svg-icon:has(.content) {\n  height: 1rem;\n  width: 1rem;\n}\n.admonition[data-callout=embed-feat] .admonition-title .admonition-title-inner {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  font-weight: 700;\n  padding-bottom: 0.125rem;\n  margin-right: 0.125rem;\n}\n.admonition[data-callout=embed-feat] .admonition-title .admonition-title-inner em {\n  font-weight: 500;\n}\n.admonition[data-callout=embed-feat] .admonition-content {\n  background-color: rgb(239, 187, 169);\n  padding: 1rem;\n}\n.admonition[data-callout=embed-feat] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-feat] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-feat] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-feat] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=embed-feat] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=embed-feat] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=embed-feat] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-feat] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=embed-feat] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-feat] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-feat] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-feat] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=embed-feat] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=embed-feat] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=embed-feat] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-feat] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=embed-feat] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-feat] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-feat] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-feat] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=embed-feat] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=embed-feat] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=embed-feat] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-feat] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=embed-feat] .admonition-content code {\n  background-color: rgb(232, 232, 232);\n  color: rgb(80, 62, 141);\n  font-family: var(--pf2e-font-monospace);\n}\n.admonition[data-callout=embed-feat] .admonition-content a.internal-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.admonition[data-callout=embed-feat] .admonition-content a.internal-link:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=embed-feat] .admonition-content a.internal-link.is-unresolved {\n  font-style: italic;\n  text-decoration: none;\n}\n.admonition[data-callout=embed-feat] .admonition-content a.internal-link.is-unresolved:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=embed-feat] .admonition-content a.external-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.admonition[data-callout=embed-feat] .admonition-content a.external-link:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=embed-feat] .admonition-content strong {\n  padding-inline: 0.25rem;\n}\n.admonition[data-callout=embed-feat] .admonition-content blockquote {\n  padding: 0.3125rem 0.3125rem 0.3125rem 0.625rem;\n  border-right: 0.1875rem solid;\n  border-left: 0.1875rem solid;\n  margin-left: 0.75em;\n  margin-right: 0.75em;\n}\n.admonition[data-callout=embed-feat] .admonition-content p {\n  line-height: 1.2;\n  margin-block-start: 0;\n  margin-block-end: 0;\n  margin-left: 0.5rem;\n  margin-bottom: 1rem;\n}\n.admonition[data-callout=embed-feat] .admonition-content > .callout {\n  mix-blend-mode: normal;\n  margin-bottom: 1rem;\n}\n.admonition[data-callout=embed-feat] .admonition-content-copy {\n  opacity: 0;\n  margin: 0.3125rem;\n  right: 0;\n  top: 0;\n  transition: 1s opacity ease-in-out;\n}\n.admonition[data-callout=embed-feat] .admonition-content-copy:hover {\n  color: rgba(232, 232, 232, 0.3);\n}\n\n.admonition[data-callout=embed-item] {\n  --callout-border-opacity: 0.3;\n  --callout-color: rgb(163, 200, 209);\n  --callout-content-background: rgb(163, 200, 209);\n  --callout-background-alpha: 0.2;\n  --callout-padding: 1rem;\n  --callout-radius: 0.25rem;\n  font-size: 1rem;\n  overflow-wrap: break-word;\n  overflow-y: scroll;\n  box-shadow: 0 0 5px 0 rgba(26, 34, 77, 0.9);\n  border-color: var(--pf2e-light-brown-headers);\n  border-style: solid;\n  border-width: 0.05rem;\n  color: rgb(36, 36, 36);\n  margin: 3px 0.125rem 0.125rem;\n  padding: 0.125rem;\n}\n.admonition[data-callout=embed-item] .admonition-title {\n  background-color: rgb(163, 200, 209);\n  flex-direction: row;\n  flex-wrap: wrap;\n  justify-content: center;\n  align-items: center;\n  align-content: center;\n  border: none;\n  display: flex;\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  position: relative;\n}\n.admonition[data-callout=embed-item] .admonition-title .admonition-title-icon .svg-icon {\n  display: block;\n  height: 1.5rem;\n  left: 0.5rem;\n  margin-left: 0.5rem;\n  position: absolute;\n  top: 0.5rem;\n  width: 1.5rem;\n}\n.admonition[data-callout=embed-item] .admonition-title .admonition-title-icon .svg-icon:has(.content) {\n  height: 1rem;\n  width: 1rem;\n}\n.admonition[data-callout=embed-item] .admonition-title .admonition-title-inner {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  font-weight: 700;\n  padding-bottom: 0.125rem;\n  margin-right: 0.125rem;\n}\n.admonition[data-callout=embed-item] .admonition-title .admonition-title-inner em {\n  font-weight: 500;\n}\n.admonition[data-callout=embed-item] .admonition-content {\n  background-color: rgb(163, 200, 209);\n  padding: 1rem;\n}\n.admonition[data-callout=embed-item] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-item] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-item] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-item] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=embed-item] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=embed-item] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=embed-item] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-item] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=embed-item] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-item] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-item] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-item] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=embed-item] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=embed-item] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=embed-item] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-item] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=embed-item] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-item] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-item] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-item] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=embed-item] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=embed-item] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=embed-item] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-item] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=embed-item] .admonition-content code {\n  background-color: rgb(232, 232, 232);\n  color: rgb(80, 62, 141);\n  font-family: var(--pf2e-font-monospace);\n}\n.admonition[data-callout=embed-item] .admonition-content a.internal-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.admonition[data-callout=embed-item] .admonition-content a.internal-link:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=embed-item] .admonition-content a.internal-link.is-unresolved {\n  font-style: italic;\n  text-decoration: none;\n}\n.admonition[data-callout=embed-item] .admonition-content a.internal-link.is-unresolved:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=embed-item] .admonition-content a.external-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.admonition[data-callout=embed-item] .admonition-content a.external-link:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=embed-item] .admonition-content strong {\n  padding-inline: 0.25rem;\n}\n.admonition[data-callout=embed-item] .admonition-content blockquote {\n  padding: 0.3125rem 0.3125rem 0.3125rem 0.625rem;\n  border-right: 0.1875rem solid;\n  border-left: 0.1875rem solid;\n  margin-left: 0.75em;\n  margin-right: 0.75em;\n}\n.admonition[data-callout=embed-item] .admonition-content p {\n  line-height: 1.2;\n  margin-block-start: 0;\n  margin-block-end: 0;\n  margin-left: 0.5rem;\n  margin-bottom: 1rem;\n}\n.admonition[data-callout=embed-item] .admonition-content > .callout {\n  mix-blend-mode: normal;\n  margin-bottom: 1rem;\n}\n.admonition[data-callout=embed-item] .admonition-content-copy {\n  opacity: 0;\n  margin: 0.3125rem;\n  right: 0;\n  top: 0;\n  transition: 1s opacity ease-in-out;\n}\n.admonition[data-callout=embed-item] .admonition-content-copy:hover {\n  color: rgba(232, 232, 232, 0.3);\n}\n\n.admonition[data-callout=embed-ritual] {\n  --callout-border-opacity: 0.3;\n  --callout-color: rgb(158, 187, 144);\n  --callout-content-background: rgb(158, 187, 144);\n  --callout-background-alpha: 0.2;\n  --callout-padding: 1rem;\n  --callout-radius: 0.25rem;\n  font-size: 1rem;\n  overflow-wrap: break-word;\n  overflow-y: scroll;\n  box-shadow: 0 0 5px 0 rgba(26, 34, 77, 0.9);\n  border-color: var(--pf2e-light-brown-headers);\n  border-style: solid;\n  border-width: 0.05rem;\n  color: rgb(36, 36, 36);\n  margin: 3px 0.125rem 0.125rem;\n  padding: 0.125rem;\n}\n.admonition[data-callout=embed-ritual] .admonition-title {\n  background-color: rgb(158, 187, 144);\n  flex-direction: row;\n  flex-wrap: wrap;\n  justify-content: center;\n  align-items: center;\n  align-content: center;\n  border: none;\n  display: flex;\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  position: relative;\n}\n.admonition[data-callout=embed-ritual] .admonition-title .admonition-title-icon .svg-icon {\n  display: block;\n  height: 1.5rem;\n  left: 0.5rem;\n  margin-left: 0.5rem;\n  position: absolute;\n  top: 0.5rem;\n  width: 1.5rem;\n}\n.admonition[data-callout=embed-ritual] .admonition-title .admonition-title-icon .svg-icon:has(.content) {\n  height: 1rem;\n  width: 1rem;\n}\n.admonition[data-callout=embed-ritual] .admonition-title .admonition-title-inner {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  font-weight: 700;\n  padding-bottom: 0.125rem;\n  margin-right: 0.125rem;\n}\n.admonition[data-callout=embed-ritual] .admonition-title .admonition-title-inner em {\n  font-weight: 500;\n}\n.admonition[data-callout=embed-ritual] .admonition-content {\n  background-color: rgb(158, 187, 144);\n  padding: 1rem;\n}\n.admonition[data-callout=embed-ritual] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ritual] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ritual] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ritual] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=embed-ritual] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=embed-ritual] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ritual] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ritual] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ritual] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ritual] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ritual] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ritual] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=embed-ritual] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=embed-ritual] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ritual] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ritual] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ritual] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ritual] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ritual] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ritual] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=embed-ritual] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=embed-ritual] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ritual] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ritual] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=embed-ritual] .admonition-content code {\n  background-color: rgb(232, 232, 232);\n  color: rgb(80, 62, 141);\n  font-family: var(--pf2e-font-monospace);\n}\n.admonition[data-callout=embed-ritual] .admonition-content a.internal-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.admonition[data-callout=embed-ritual] .admonition-content a.internal-link:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=embed-ritual] .admonition-content a.internal-link.is-unresolved {\n  font-style: italic;\n  text-decoration: none;\n}\n.admonition[data-callout=embed-ritual] .admonition-content a.internal-link.is-unresolved:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=embed-ritual] .admonition-content a.external-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.admonition[data-callout=embed-ritual] .admonition-content a.external-link:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=embed-ritual] .admonition-content strong {\n  padding-inline: 0.25rem;\n}\n.admonition[data-callout=embed-ritual] .admonition-content blockquote {\n  padding: 0.3125rem 0.3125rem 0.3125rem 0.625rem;\n  border-right: 0.1875rem solid;\n  border-left: 0.1875rem solid;\n  margin-left: 0.75em;\n  margin-right: 0.75em;\n}\n.admonition[data-callout=embed-ritual] .admonition-content p {\n  line-height: 1.2;\n  margin-block-start: 0;\n  margin-block-end: 0;\n  margin-left: 0.5rem;\n  margin-bottom: 1rem;\n}\n.admonition[data-callout=embed-ritual] .admonition-content > .callout {\n  mix-blend-mode: normal;\n  margin-bottom: 1rem;\n}\n.admonition[data-callout=embed-ritual] .admonition-content-copy {\n  opacity: 0;\n  margin: 0.3125rem;\n  right: 0;\n  top: 0;\n  transition: 1s opacity ease-in-out;\n}\n.admonition[data-callout=embed-ritual] .admonition-content-copy:hover {\n  color: rgba(232, 232, 232, 0.3);\n}\n\n.admonition[data-callout=inline-affliction] {\n  --callout-border-opacity: 0.3;\n  --callout-color: rgb(218, 216, 218);\n  --callout-content-background: rgb(218, 216, 218);\n  --callout-background-alpha: 0.2;\n  --callout-padding: 1rem;\n  --callout-radius: 0.25rem;\n  font-size: 1rem;\n  overflow-wrap: break-word;\n  overflow-y: scroll;\n  box-shadow: 0 0 5px 0 rgba(26, 34, 77, 0.9);\n  border-color: var(--pf2e-light-brown-headers);\n  border-style: solid;\n  border-width: 0.05rem;\n  color: rgb(36, 36, 36);\n  margin: 3px 0.125rem 0.125rem;\n  padding: 0.125rem;\n}\n.admonition[data-callout=inline-affliction] .admonition-title {\n  background-color: rgb(218, 216, 218);\n  flex-direction: row;\n  flex-wrap: wrap;\n  justify-content: center;\n  align-items: center;\n  align-content: center;\n  border: none;\n  display: flex;\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  position: relative;\n}\n.admonition[data-callout=inline-affliction] .admonition-title .admonition-title-icon .svg-icon {\n  display: block;\n  height: 1.5rem;\n  left: 0.5rem;\n  margin-left: 0.5rem;\n  position: absolute;\n  top: 0.5rem;\n  width: 1.5rem;\n}\n.admonition[data-callout=inline-affliction] .admonition-title .admonition-title-icon .svg-icon:has(.content) {\n  height: 1rem;\n  width: 1rem;\n}\n.admonition[data-callout=inline-affliction] .admonition-title .admonition-title-inner {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  font-weight: 700;\n  padding-bottom: 0.125rem;\n  margin-right: 0.125rem;\n}\n.admonition[data-callout=inline-affliction] .admonition-title .admonition-title-inner em {\n  font-weight: 500;\n}\n.admonition[data-callout=inline-affliction] .admonition-content {\n  background-color: rgb(218, 216, 218);\n  padding: 1rem;\n}\n.admonition[data-callout=inline-affliction] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=inline-affliction] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=inline-affliction] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=inline-affliction] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=inline-affliction] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=inline-affliction] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=inline-affliction] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=inline-affliction] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=inline-affliction] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=inline-affliction] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=inline-affliction] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=inline-affliction] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=inline-affliction] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=inline-affliction] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=inline-affliction] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=inline-affliction] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=inline-affliction] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=inline-affliction] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=inline-affliction] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=inline-affliction] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=inline-affliction] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=inline-affliction] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=inline-affliction] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=inline-affliction] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=inline-affliction] .admonition-content code {\n  background-color: rgb(232, 232, 232);\n  color: rgb(80, 62, 141);\n  font-family: var(--pf2e-font-monospace);\n}\n.admonition[data-callout=inline-affliction] .admonition-content a.internal-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.admonition[data-callout=inline-affliction] .admonition-content a.internal-link:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=inline-affliction] .admonition-content a.internal-link.is-unresolved {\n  font-style: italic;\n  text-decoration: none;\n}\n.admonition[data-callout=inline-affliction] .admonition-content a.internal-link.is-unresolved:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=inline-affliction] .admonition-content a.external-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.admonition[data-callout=inline-affliction] .admonition-content a.external-link:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=inline-affliction] .admonition-content strong {\n  padding-inline: 0.25rem;\n}\n.admonition[data-callout=inline-affliction] .admonition-content blockquote {\n  padding: 0.3125rem 0.3125rem 0.3125rem 0.625rem;\n  border-right: 0.1875rem solid;\n  border-left: 0.1875rem solid;\n  margin-left: 0.75em;\n  margin-right: 0.75em;\n}\n.admonition[data-callout=inline-affliction] .admonition-content p {\n  line-height: 1.2;\n  margin-block-start: 0;\n  margin-block-end: 0;\n  margin-left: 0.5rem;\n  margin-bottom: 1rem;\n}\n.admonition[data-callout=inline-affliction] .admonition-content > .callout {\n  mix-blend-mode: normal;\n  margin-bottom: 1rem;\n}\n.admonition[data-callout=inline-affliction] .admonition-content-copy {\n  opacity: 0;\n  margin: 0.3125rem;\n  right: 0;\n  top: 0;\n  transition: 1s opacity ease-in-out;\n}\n.admonition[data-callout=inline-affliction] .admonition-content-copy:hover {\n  color: rgba(232, 232, 232, 0.3);\n}\n\n.admonition[data-callout=inline-attack] {\n  --callout-border-opacity: 0.3;\n  --callout-color: rgb(245, 229, 188);\n  --callout-content-background: rgb(245, 229, 188);\n  --callout-background-alpha: 0.2;\n  --callout-padding: 1rem;\n  --callout-radius: 0.25rem;\n  font-size: 1rem;\n  overflow-wrap: break-word;\n  overflow-y: scroll;\n  box-shadow: 0 0 5px 0 rgba(26, 34, 77, 0.9);\n  border-color: var(--pf2e-light-brown-headers);\n  border-style: solid;\n  border-width: 0.05rem;\n  color: rgb(36, 36, 36);\n  margin: 3px 0.125rem 0.125rem;\n  padding: 0.125rem;\n}\n.admonition[data-callout=inline-attack] .admonition-title {\n  background-color: rgb(245, 229, 188);\n  flex-direction: row;\n  flex-wrap: wrap;\n  justify-content: center;\n  align-items: center;\n  align-content: center;\n  border: none;\n  display: flex;\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  position: relative;\n}\n.admonition[data-callout=inline-attack] .admonition-title .admonition-title-icon .svg-icon {\n  display: block;\n  height: 1.5rem;\n  left: 0.5rem;\n  margin-left: 0.5rem;\n  position: absolute;\n  top: 0.5rem;\n  width: 1.5rem;\n}\n.admonition[data-callout=inline-attack] .admonition-title .admonition-title-icon .svg-icon:has(.content) {\n  height: 1rem;\n  width: 1rem;\n}\n.admonition[data-callout=inline-attack] .admonition-title .admonition-title-inner {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  font-weight: 700;\n  padding-bottom: 0.125rem;\n  margin-right: 0.125rem;\n}\n.admonition[data-callout=inline-attack] .admonition-title .admonition-title-inner em {\n  font-weight: 500;\n}\n.admonition[data-callout=inline-attack] .admonition-content {\n  background-color: rgb(245, 229, 188);\n  padding: 1rem;\n}\n.admonition[data-callout=inline-attack] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=inline-attack] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=inline-attack] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=inline-attack] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=inline-attack] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=inline-attack] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=inline-attack] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=inline-attack] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=inline-attack] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=inline-attack] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=inline-attack] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=inline-attack] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=inline-attack] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=inline-attack] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=inline-attack] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=inline-attack] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=inline-attack] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=inline-attack] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=inline-attack] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=inline-attack] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=inline-attack] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=inline-attack] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=inline-attack] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=inline-attack] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=inline-attack] .admonition-content code {\n  background-color: rgb(232, 232, 232);\n  color: rgb(80, 62, 141);\n  font-family: var(--pf2e-font-monospace);\n}\n.admonition[data-callout=inline-attack] .admonition-content a.internal-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.admonition[data-callout=inline-attack] .admonition-content a.internal-link:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=inline-attack] .admonition-content a.internal-link.is-unresolved {\n  font-style: italic;\n  text-decoration: none;\n}\n.admonition[data-callout=inline-attack] .admonition-content a.internal-link.is-unresolved:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=inline-attack] .admonition-content a.external-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.admonition[data-callout=inline-attack] .admonition-content a.external-link:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=inline-attack] .admonition-content strong {\n  padding-inline: 0.25rem;\n}\n.admonition[data-callout=inline-attack] .admonition-content blockquote {\n  padding: 0.3125rem 0.3125rem 0.3125rem 0.625rem;\n  border-right: 0.1875rem solid;\n  border-left: 0.1875rem solid;\n  margin-left: 0.75em;\n  margin-right: 0.75em;\n}\n.admonition[data-callout=inline-attack] .admonition-content p {\n  line-height: 1.2;\n  margin-block-start: 0;\n  margin-block-end: 0;\n  margin-left: 0.5rem;\n  margin-bottom: 1rem;\n}\n.admonition[data-callout=inline-attack] .admonition-content > .callout {\n  mix-blend-mode: normal;\n  margin-bottom: 1rem;\n}\n.admonition[data-callout=inline-attack] .admonition-content-copy {\n  opacity: 0;\n  margin: 0.3125rem;\n  right: 0;\n  top: 0;\n  transition: 1s opacity ease-in-out;\n}\n.admonition[data-callout=inline-attack] .admonition-content-copy:hover {\n  color: rgba(232, 232, 232, 0.3);\n}\n\n.admonition[data-callout=pf2-summary] {\n  --callout-border-opacity: 0.3;\n  --callout-color: rgb(209, 224, 224);\n  --callout-content-background: rgb(209, 224, 224);\n  --callout-background-alpha: 0.2;\n  --callout-padding: 1rem;\n  --callout-radius: 0.25rem;\n  font-size: 1rem;\n  overflow-wrap: break-word;\n  overflow-y: scroll;\n  box-shadow: 0 0 5px 0 rgba(26, 34, 77, 0.9);\n  border-color: var(--pf2e-light-brown-headers);\n  border-style: solid;\n  border-width: 0.05rem;\n  color: rgb(36, 36, 36);\n  margin: 3px 0.125rem 0.125rem;\n  padding: 0.125rem;\n}\n.admonition[data-callout=pf2-summary] .admonition-title {\n  background-color: rgb(209, 224, 224);\n  flex-direction: row;\n  flex-wrap: wrap;\n  justify-content: center;\n  align-items: center;\n  align-content: center;\n  border: none;\n  display: flex;\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  position: relative;\n}\n.admonition[data-callout=pf2-summary] .admonition-title .admonition-title-icon .svg-icon {\n  display: block;\n  height: 1.5rem;\n  left: 0.5rem;\n  margin-left: 0.5rem;\n  position: absolute;\n  top: 0.5rem;\n  width: 1.5rem;\n}\n.admonition[data-callout=pf2-summary] .admonition-title .admonition-title-icon .svg-icon:has(.content) {\n  height: 1rem;\n  width: 1rem;\n}\n.admonition[data-callout=pf2-summary] .admonition-title .admonition-title-inner {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  font-weight: 700;\n  padding-bottom: 0.125rem;\n  margin-right: 0.125rem;\n}\n.admonition[data-callout=pf2-summary] .admonition-title .admonition-title-inner em {\n  font-weight: 500;\n}\n.admonition[data-callout=pf2-summary] .admonition-content {\n  background-color: rgb(209, 224, 224);\n  padding: 1rem;\n}\n.admonition[data-callout=pf2-summary] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-summary] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-summary] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-summary] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=pf2-summary] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=pf2-summary] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-summary] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-summary] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-summary] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-summary] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-summary] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-summary] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=pf2-summary] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=pf2-summary] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-summary] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-summary] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-summary] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-summary] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-summary] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-summary] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=pf2-summary] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=pf2-summary] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-summary] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-summary] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=pf2-summary] .admonition-content code {\n  background-color: rgb(232, 232, 232);\n  color: rgb(80, 62, 141);\n  font-family: var(--pf2e-font-monospace);\n}\n.admonition[data-callout=pf2-summary] .admonition-content a.internal-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.admonition[data-callout=pf2-summary] .admonition-content a.internal-link:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=pf2-summary] .admonition-content a.internal-link.is-unresolved {\n  font-style: italic;\n  text-decoration: none;\n}\n.admonition[data-callout=pf2-summary] .admonition-content a.internal-link.is-unresolved:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=pf2-summary] .admonition-content a.external-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.admonition[data-callout=pf2-summary] .admonition-content a.external-link:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=pf2-summary] .admonition-content strong {\n  padding-inline: 0.25rem;\n}\n.admonition[data-callout=pf2-summary] .admonition-content blockquote {\n  padding: 0.3125rem 0.3125rem 0.3125rem 0.625rem;\n  border-right: 0.1875rem solid;\n  border-left: 0.1875rem solid;\n  margin-left: 0.75em;\n  margin-right: 0.75em;\n}\n.admonition[data-callout=pf2-summary] .admonition-content p {\n  line-height: 1.2;\n  margin-block-start: 0;\n  margin-block-end: 0;\n  margin-left: 0.5rem;\n  margin-bottom: 1rem;\n}\n.admonition[data-callout=pf2-summary] .admonition-content > .callout {\n  mix-blend-mode: normal;\n  margin-bottom: 1rem;\n}\n.admonition[data-callout=pf2-summary] .admonition-content-copy {\n  opacity: 0;\n  margin: 0.3125rem;\n  right: 0;\n  top: 0;\n  transition: 1s opacity ease-in-out;\n}\n.admonition[data-callout=pf2-summary] .admonition-content-copy:hover {\n  color: rgba(232, 232, 232, 0.3);\n}\n\n.admonition[data-callout=statblock-pf2] {\n  --callout-border-opacity: 0.3;\n  --callout-color: rgb(239, 215, 143);\n  --callout-content-background: rgb(239, 215, 143);\n  --callout-background-alpha: 0.2;\n  --callout-padding: 1rem;\n  --callout-radius: 0.25rem;\n  font-size: 1rem;\n  overflow-wrap: break-word;\n  overflow-y: scroll;\n  box-shadow: 0 0 5px 0 rgba(26, 34, 77, 0.9);\n  border-color: var(--pf2e-light-brown-headers);\n  border-style: solid;\n  border-width: 0.05rem;\n  color: rgb(36, 36, 36);\n  margin: 3px 0.125rem 0.125rem;\n  padding: 0.125rem;\n}\n.admonition[data-callout=statblock-pf2] .admonition-title {\n  background-color: rgb(239, 215, 143);\n  flex-direction: row;\n  flex-wrap: wrap;\n  justify-content: center;\n  align-items: center;\n  align-content: center;\n  border: none;\n  display: flex;\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  position: relative;\n}\n.admonition[data-callout=statblock-pf2] .admonition-title .admonition-title-icon .svg-icon {\n  display: block;\n  height: 1.5rem;\n  left: 0.5rem;\n  margin-left: 0.5rem;\n  position: absolute;\n  top: 0.5rem;\n  width: 1.5rem;\n}\n.admonition[data-callout=statblock-pf2] .admonition-title .admonition-title-icon .svg-icon:has(.content) {\n  height: 1rem;\n  width: 1rem;\n}\n.admonition[data-callout=statblock-pf2] .admonition-title .admonition-title-inner {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  font-weight: 700;\n  padding-bottom: 0.125rem;\n  margin-right: 0.125rem;\n}\n.admonition[data-callout=statblock-pf2] .admonition-title .admonition-title-inner em {\n  font-weight: 500;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content {\n  background-color: rgb(239, 215, 143);\n  padding: 1rem;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content h1 {\n  font-size: clamp(1.125rem, 1.5em, 1.375rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content h2 {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content h3 {\n  font-size: clamp(0.875rem, 1.125em, 1rem);\n  padding-bottom: 0.125em;\n  text-align: center;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content h3::before {\n  display: none;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content h3::after {\n  display: none;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content h4 {\n  font-size: clamp(0.75rem, 1em, 0.875rem);\n  padding-bottom: 0.5em;\n  text-align: center;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content h5 {\n  font-size: clamp(0.625rem, 0.9375em, 0.75rem);\n  padding-bottom: 0.25em;\n  text-align: center;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content h6 {\n  font-size: clamp(0.8125em, 0.5rem, 0.625rem);\n  padding-bottom: 0.75em;\n  text-align: center;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content code {\n  background-color: rgb(232, 232, 232);\n  color: rgb(80, 62, 141);\n  font-family: var(--pf2e-font-monospace);\n}\n.admonition[data-callout=statblock-pf2] .admonition-content a.internal-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content a.internal-link:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content a.internal-link.is-unresolved {\n  font-style: italic;\n  text-decoration: none;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content a.internal-link.is-unresolved:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content a.external-link {\n  font-weight: 600;\n  text-decoration: none;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content a.external-link:hover {\n  text-decoration: underline;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content strong {\n  padding-inline: 0.25rem;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content blockquote {\n  padding: 0.3125rem 0.3125rem 0.3125rem 0.625rem;\n  border-right: 0.1875rem solid;\n  border-left: 0.1875rem solid;\n  margin-left: 0.75em;\n  margin-right: 0.75em;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content p {\n  line-height: 1.2;\n  margin-block-start: 0;\n  margin-block-end: 0;\n  margin-left: 0.5rem;\n  margin-bottom: 1rem;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content > .callout {\n  mix-blend-mode: normal;\n  margin-bottom: 1rem;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content-copy {\n  opacity: 0;\n  margin: 0.3125rem;\n  right: 0;\n  top: 0;\n  transition: 1s opacity ease-in-out;\n}\n.admonition[data-callout=statblock-pf2] .admonition-content-copy:hover {\n  color: rgba(232, 232, 232, 0.3);\n}\n\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n.callout[data-callout=pf2-beige] .callout-title .callout-title-inner {\n  margin-left: 0.25rem;\n  padding-bottom: 0.25rem;\n}\n.callout[data-callout=pf2-beige] .callout-content a.internal-link {\n  color: rgb(102, 51, 51);\n}\n.callout[data-callout=pf2-beige] .callout-content a.internal-link:hover {\n  color: #aa5555;\n}\n.callout[data-callout=pf2-beige] .callout-content a.internal-link.is-unresolved {\n  color: rgb(0, 73, 97);\n}\n.callout[data-callout=pf2-beige] .callout-content a.internal-link.is-unresolved:hover {\n  color: rgb(0, 149.7628865979, 199);\n}\n.callout[data-callout=pf2-beige] .callout-content a.internal-link:is([href*=\"#Actions\"]) {\n  color: transparent;\n}\n.callout[data-callout=pf2-beige] .callout-content a.internal-link:is([href*=\"#Actions\"]):has(.admonition) {\n  vertical-align: sub;\n}\n.callout[data-callout=pf2-beige] .callout-content a.external-link {\n  color: rgb(107, 0, 153);\n}\n.callout[data-callout=pf2-beige] .callout-content a.external-link:hover {\n  color: rgb(178.3333333333, 0, 255);\n}\n.callout[data-callout=pf2-beige] .callout-content em {\n  color: rgb(51, 51, 51);\n}\n.callout[data-callout=pf2-beige] .callout-content h1 {\n  color: rgb(80, 67, 79);\n}\n.callout[data-callout=pf2-beige] .callout-content h2 {\n  color: rgb(44, 70, 78);\n}\n.callout[data-callout=pf2-beige] .callout-content h3 {\n  color: rgb(51, 51, 51);\n}\n.callout[data-callout=pf2-beige] .callout-content h4 {\n  color: rgb(42, 72, 62);\n}\n.callout[data-callout=pf2-beige] .callout-content h5 {\n  color: rgb(102, 51, 51);\n}\n.callout[data-callout=pf2-beige] .callout-content h6 {\n  color: rgb(36, 36, 36);\n}\n.callout[data-callout=pf2-beige] .callout-content strong {\n  color: rgb(51, 66, 82);\n}\n.callout[data-callout=pf2-beige] .callout-content strong em {\n  color: rgb(51, 58.5, 66.5);\n}\n.callout[data-callout=pf2-beige] .callout-content ol > li::marker,\n.callout[data-callout=pf2-beige] .callout-content ul > li::marker {\n  color: rgb(204, 72, 0);\n}\n.callout[data-callout=pf2-beige] .callout-content h1, .callout[data-callout=pf2-beige] .callout-content h2, .callout[data-callout=pf2-beige] .callout-content h3, .callout[data-callout=pf2-beige] .callout-content h4, .callout[data-callout=pf2-beige] .callout-content h5, .callout[data-callout=pf2-beige] .callout-content h6 {\n  text-align: left;\n  padding-left: 0.25rem;\n}\n\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n.callout[data-callout=pf2-brown] .callout-title .callout-title-inner {\n  font-family: \"Helvetica Neue\", Helvetica, Arial, \"Pathfinder\", sans-serif;\n  margin-left: 0.25rem;\n  text-align: center;\n}\n.callout[data-callout=pf2-brown] .callout-content a.internal-link {\n  color: rgb(102, 51, 51);\n}\n.callout[data-callout=pf2-brown] .callout-content a.internal-link:hover {\n  color: #aa5555;\n}\n.callout[data-callout=pf2-brown] .callout-content a.internal-link.is-unresolved {\n  color: rgb(0, 73, 97);\n}\n.callout[data-callout=pf2-brown] .callout-content a.internal-link.is-unresolved:hover {\n  color: rgb(0, 149.7628865979, 199);\n}\n.callout[data-callout=pf2-brown] .callout-content a.internal-link:is([href*=\"#Actions\"]) {\n  color: transparent;\n}\n.callout[data-callout=pf2-brown] .callout-content a.internal-link:is([href*=\"#Actions\"]):has(.admonition) {\n  vertical-align: sub;\n}\n.callout[data-callout=pf2-brown] .callout-content a.external-link {\n  color: rgb(107, 0, 153);\n}\n.callout[data-callout=pf2-brown] .callout-content a.external-link:hover {\n  color: rgb(178.3333333333, 0, 255);\n}\n.callout[data-callout=pf2-brown] .callout-content em {\n  color: rgb(51, 51, 51);\n}\n.callout[data-callout=pf2-brown] .callout-content h1 {\n  color: rgb(80, 67, 79);\n}\n.callout[data-callout=pf2-brown] .callout-content h2 {\n  color: rgb(44, 70, 78);\n}\n.callout[data-callout=pf2-brown] .callout-content h3 {\n  color: rgb(51, 51, 51);\n}\n.callout[data-callout=pf2-brown] .callout-content h4 {\n  color: rgb(42, 72, 62);\n}\n.callout[data-callout=pf2-brown] .callout-content h5 {\n  color: rgb(102, 51, 51);\n}\n.callout[data-callout=pf2-brown] .callout-content h6 {\n  color: rgb(36, 36, 36);\n}\n.callout[data-callout=pf2-brown] .callout-content strong {\n  color: rgb(51, 66, 82);\n}\n.callout[data-callout=pf2-brown] .callout-content strong em {\n  color: rgb(51, 58.5, 66.5);\n}\n.callout[data-callout=pf2-brown] .callout-content ol > li::marker,\n.callout[data-callout=pf2-brown] .callout-content ul > li::marker {\n  color: rgb(204, 72, 0);\n}\n\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n.callout.callout[data-callout=pf2-example] .callout-content a.internal-link {\n  color: rgb(102, 51, 51);\n}\n.callout.callout[data-callout=pf2-example] .callout-content a.internal-link:hover {\n  color: #aa5555;\n}\n.callout.callout[data-callout=pf2-example] .callout-content a.internal-link.is-unresolved {\n  color: rgb(0, 73, 97);\n}\n.callout.callout[data-callout=pf2-example] .callout-content a.internal-link.is-unresolved:hover {\n  color: rgb(0, 149.7628865979, 199);\n}\n.callout.callout[data-callout=pf2-example] .callout-content a.internal-link:is([href*=\"#Actions\"]) {\n  color: transparent;\n}\n.callout.callout[data-callout=pf2-example] .callout-content a.internal-link:is([href*=\"#Actions\"]):has(.admonition) {\n  vertical-align: sub;\n}\n.callout.callout[data-callout=pf2-example] .callout-content a.external-link {\n  color: rgb(107, 0, 153);\n}\n.callout.callout[data-callout=pf2-example] .callout-content a.external-link:hover {\n  color: rgb(178.3333333333, 0, 255);\n}\n.callout.callout[data-callout=pf2-example] .callout-content em {\n  color: rgb(51, 51, 51);\n}\n.callout.callout[data-callout=pf2-example] .callout-content h1 {\n  color: rgb(80, 67, 79);\n}\n.callout.callout[data-callout=pf2-example] .callout-content h2 {\n  color: rgb(44, 70, 78);\n}\n.callout.callout[data-callout=pf2-example] .callout-content h3 {\n  color: rgb(51, 51, 51);\n}\n.callout.callout[data-callout=pf2-example] .callout-content h4 {\n  color: rgb(42, 72, 62);\n}\n.callout.callout[data-callout=pf2-example] .callout-content h5 {\n  color: rgb(102, 51, 51);\n}\n.callout.callout[data-callout=pf2-example] .callout-content h6 {\n  color: rgb(36, 36, 36);\n}\n.callout.callout[data-callout=pf2-example] .callout-content strong {\n  color: rgb(51, 66, 82);\n}\n.callout.callout[data-callout=pf2-example] .callout-content strong em {\n  color: rgb(51, 58.5, 66.5);\n}\n.callout.callout[data-callout=pf2-example] .callout-content ol > li::marker,\n.callout.callout[data-callout=pf2-example] .callout-content ul > li::marker {\n  color: rgb(204, 72, 0);\n}\n\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n.callout[data-callout=pf2-inset] .callout-title .callout-title-inner {\n  display: none;\n}\n.callout[data-callout=pf2-inset] .callout-content {\n  font-weight: 700;\n}\n\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n.callout[data-callout=pf2-key-box] {\n  color: rgb(232, 232, 232);\n  clear: left;\n  display: inline-flex;\n  float-wrap: wrap;\n  float: left;\n  font-family: \"Times New Roman\", Times, serif;\n  text-transform: capitalize;\n}\n.callout[data-callout=pf2-key-box] .callout-content a.internal-link {\n  color: rgb(255, 219, 88);\n}\n.callout[data-callout=pf2-key-box] .callout-content a.internal-link:hover {\n  color: rgb(241, 189.0479041916, 0);\n}\n.callout[data-callout=pf2-key-box] .callout-content a.internal-link.is-unresolved {\n  color: rgb(179.25, 208.5, 59.5);\n}\n.callout[data-callout=pf2-key-box] .callout-content a.internal-link.is-unresolved:hover {\n  color: rgb(114.0392561983, 134.1033057851, 31.8966942149);\n}\n.callout[data-callout=pf2-key-box] .callout-content a.external-link {\n  color: rgb(var(--callout-color));\n  filter: invert(100%);\n}\n.callout[data-callout=pf2-key-box] .callout-content a.external-link:hover {\n  filter: invert(100%) brightness(1.2);\n}\n.callout[data-callout=pf2-key-box] .callout-content em {\n  color: rgb(154, 205, 50);\n}\n.callout[data-callout=pf2-key-box] .callout-content h1, .callout[data-callout=pf2-key-box] .callout-content h2, .callout[data-callout=pf2-key-box] .callout-content h3, .callout[data-callout=pf2-key-box] .callout-content h4, .callout[data-callout=pf2-key-box] .callout-content h5, .callout[data-callout=pf2-key-box] .callout-content h6 {\n  color: rgb(232, 232, 232);\n}\n.callout[data-callout=pf2-key-box] .callout-content p {\n  color: rgb(232, 232, 232);\n}\n.callout[data-callout=pf2-key-box] .callout-content strong {\n  color: rgb(255, 143, 143);\n}\n.callout[data-callout=pf2-key-box] .callout-content strong em {\n  color: rgb(204.5, 174, 96.5);\n}\n.callout[data-callout=pf2-key-box] .callout-content ol > li::marker,\n.callout[data-callout=pf2-key-box] .callout-content ul > li::marker {\n  color: rgb(204, 72, 0);\n}\n.callout[data-callout=pf2-key-box] .callout-content ul {\n  margin-block-end: unset;\n  margin-block-start: unset;\n  padding: 0.5em;\n}\n\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n.callout[data-callout=pf2-note] {\n  clear: both;\n  font-style: italic;\n  text-align: center;\n  margin-bottom: 1rem;\n}\n.callout[data-callout=pf2-note] .callout-title .callout-title-inner {\n  flex: unset;\n  display: none;\n}\n.admonition-parent .callout[data-callout=pf2-note] .callout-title .callout-title-inner {\n  display: inline;\n}\n.callout[data-callout=pf2-note] .callout-content {\n  margin-bottom: -0.5rem;\n}\n.callout[data-callout=pf2-note] .callout-content a.internal-link {\n  color: rgb(102, 51, 51);\n}\n.callout[data-callout=pf2-note] .callout-content a.internal-link:hover {\n  color: #aa5555;\n}\n.callout[data-callout=pf2-note] .callout-content a.internal-link.is-unresolved {\n  color: rgb(0, 73, 97);\n}\n.callout[data-callout=pf2-note] .callout-content a.internal-link.is-unresolved:hover {\n  color: rgb(0, 149.7628865979, 199);\n}\n.callout[data-callout=pf2-note] .callout-content a.internal-link:is([href*=\"#Actions\"]) {\n  color: transparent;\n}\n.callout[data-callout=pf2-note] .callout-content a.internal-link:is([href*=\"#Actions\"]):has(.admonition) {\n  vertical-align: sub;\n}\n.callout[data-callout=pf2-note] .callout-content a.external-link {\n  color: rgb(107, 0, 153);\n}\n.callout[data-callout=pf2-note] .callout-content a.external-link:hover {\n  color: rgb(178.3333333333, 0, 255);\n}\n.callout[data-callout=pf2-note] .callout-content em {\n  color: rgb(51, 51, 51);\n}\n.callout[data-callout=pf2-note] .callout-content h1 {\n  color: rgb(80, 67, 79);\n}\n.callout[data-callout=pf2-note] .callout-content h2 {\n  color: rgb(44, 70, 78);\n}\n.callout[data-callout=pf2-note] .callout-content h3 {\n  color: rgb(51, 51, 51);\n}\n.callout[data-callout=pf2-note] .callout-content h4 {\n  color: rgb(42, 72, 62);\n}\n.callout[data-callout=pf2-note] .callout-content h5 {\n  color: rgb(102, 51, 51);\n}\n.callout[data-callout=pf2-note] .callout-content h6 {\n  color: rgb(36, 36, 36);\n}\n.callout[data-callout=pf2-note] .callout-content strong {\n  color: rgb(51, 66, 82);\n}\n.callout[data-callout=pf2-note] .callout-content strong em {\n  color: rgb(51, 58.5, 66.5);\n}\n.callout[data-callout=pf2-note] .callout-content ol > li::marker,\n.callout[data-callout=pf2-note] .callout-content ul > li::marker {\n  color: rgb(204, 72, 0);\n}\n\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n.callout[data-callout=pf2-red] {\n  color: rgb(232, 232, 232);\n}\n.callout[data-callout=pf2-red] .callout-title .callout-title-inner {\n  font-family: \"Times New Roman\", Times, serif;\n}\n.callout[data-callout=pf2-red] .callout-title .callout-title-inner strong {\n  color: rgb(255, 143, 143);\n}\n.callout[data-callout=pf2-red] .callout-title .callout-title-inner a {\n  color: rgb(204, 72, 0);\n}\n.callout[data-callout=pf2-red] .callout-content a.internal-link {\n  color: rgb(255, 219, 88);\n}\n.callout[data-callout=pf2-red] .callout-content a.internal-link:hover {\n  color: rgb(241, 189.0479041916, 0);\n}\n.callout[data-callout=pf2-red] .callout-content a.internal-link.is-unresolved {\n  color: rgb(179.25, 208.5, 59.5);\n}\n.callout[data-callout=pf2-red] .callout-content a.internal-link.is-unresolved:hover {\n  color: rgb(114.0392561983, 134.1033057851, 31.8966942149);\n}\n.callout[data-callout=pf2-red] .callout-content a.external-link {\n  color: rgb(var(--callout-color));\n  filter: invert(100%);\n}\n.callout[data-callout=pf2-red] .callout-content a.external-link:hover {\n  filter: invert(100%) brightness(1.2);\n}\n.callout[data-callout=pf2-red] .callout-content em {\n  color: rgb(154, 205, 50);\n}\n.callout[data-callout=pf2-red] .callout-content h1, .callout[data-callout=pf2-red] .callout-content h2, .callout[data-callout=pf2-red] .callout-content h3, .callout[data-callout=pf2-red] .callout-content h4, .callout[data-callout=pf2-red] .callout-content h5, .callout[data-callout=pf2-red] .callout-content h6 {\n  color: rgb(232, 232, 232);\n}\n.callout[data-callout=pf2-red] .callout-content p {\n  color: rgb(232, 232, 232);\n}\n.callout[data-callout=pf2-red] .callout-content strong {\n  color: rgb(255, 143, 143);\n}\n.callout[data-callout=pf2-red] .callout-content strong em {\n  color: rgb(204.5, 174, 96.5);\n}\n.callout[data-callout=pf2-red] .callout-content ol > li::marker,\n.callout[data-callout=pf2-red] .callout-content ul > li::marker {\n  color: rgb(204, 72, 0);\n}\n.callout[data-callout=pf2-red] .callout-content ul {\n  margin-block-end: unset;\n  margin-block-start: unset;\n  padding: 0.5em;\n}\n.callout[data-callout=pf2-red] .callout-content table th {\n  color: rgb(232, 232, 232);\n}\n\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n.callout[data-callout=pf2-sidebar] {\n  color: rgb(128, 0, 0);\n  clear: right;\n  float: right;\n  max-height: 85vh;\n  max-width: 50%;\n  overflow-wrap: break-word;\n  overflow-y: scroll;\n}\n.callout[data-callout=pf2-sidebar] .callout-content a.internal-link {\n  color: rgb(102, 51, 51);\n}\n.callout[data-callout=pf2-sidebar] .callout-content a.internal-link:hover {\n  color: #aa5555;\n}\n.callout[data-callout=pf2-sidebar] .callout-content a.internal-link.is-unresolved {\n  color: rgb(0, 73, 97);\n}\n.callout[data-callout=pf2-sidebar] .callout-content a.internal-link.is-unresolved:hover {\n  color: rgb(0, 149.7628865979, 199);\n}\n.callout[data-callout=pf2-sidebar] .callout-content a.internal-link:is([href*=\"#Actions\"]) {\n  color: transparent;\n}\n.callout[data-callout=pf2-sidebar] .callout-content a.internal-link:is([href*=\"#Actions\"]):has(.admonition) {\n  vertical-align: sub;\n}\n.callout[data-callout=pf2-sidebar] .callout-content a.external-link {\n  color: rgb(107, 0, 153);\n}\n.callout[data-callout=pf2-sidebar] .callout-content a.external-link:hover {\n  color: rgb(178.3333333333, 0, 255);\n}\n.callout[data-callout=pf2-sidebar] .callout-content em {\n  color: rgb(51, 51, 51);\n}\n.callout[data-callout=pf2-sidebar] .callout-content h1 {\n  color: rgb(80, 67, 79);\n}\n.callout[data-callout=pf2-sidebar] .callout-content h2 {\n  color: rgb(44, 70, 78);\n}\n.callout[data-callout=pf2-sidebar] .callout-content h3 {\n  color: rgb(51, 51, 51);\n}\n.callout[data-callout=pf2-sidebar] .callout-content h4 {\n  color: rgb(42, 72, 62);\n}\n.callout[data-callout=pf2-sidebar] .callout-content h5 {\n  color: rgb(102, 51, 51);\n}\n.callout[data-callout=pf2-sidebar] .callout-content h6 {\n  color: rgb(36, 36, 36);\n}\n.callout[data-callout=pf2-sidebar] .callout-content strong {\n  color: rgb(51, 66, 82);\n}\n.callout[data-callout=pf2-sidebar] .callout-content strong em {\n  color: rgb(51, 58.5, 66.5);\n}\n.callout[data-callout=pf2-sidebar] .callout-content ol > li::marker,\n.callout[data-callout=pf2-sidebar] .callout-content ul > li::marker {\n  color: rgb(204, 72, 0);\n}\n\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n.callout[data-callout=pf2-tip] {\n  color: rgb(36, 36, 36);\n}\n.callout[data-callout=pf2-tip] em {\n  color: rgb(224.5, 218, 186);\n}\n.callout[data-callout=pf2-tip] strong {\n  color: rgb(141.5, 149, 157);\n}\n.callout[data-callout=pf2-tip] strong em {\n  color: rgb(51, 58.5, 66.5);\n}\n.callout[data-callout=pf2-tip] .callout-title {\n  background-color: rgb(6, 20, 53);\n}\n.callout[data-callout=pf2-tip] .callout-title .callout-title-inner {\n  color: rgb(217, 204, 140);\n  margin-left: 0.25rem;\n  text-align: left;\n}\n.callout[data-callout=pf2-tip] .callout-title .callout-title-inner a {\n  color: #aa5555;\n}\n.callout[data-callout=pf2-tip] .callout-content a.internal-link {\n  color: rgb(102, 51, 51);\n}\n.callout[data-callout=pf2-tip] .callout-content a.internal-link:hover {\n  color: #aa5555;\n}\n.callout[data-callout=pf2-tip] .callout-content a.internal-link.is-unresolved {\n  color: rgb(0, 73, 97);\n}\n.callout[data-callout=pf2-tip] .callout-content a.internal-link.is-unresolved:hover {\n  color: rgb(0, 149.7628865979, 199);\n}\n.callout[data-callout=pf2-tip] .callout-content a.external-link {\n  color: rgb(107, 0, 153);\n}\n.callout[data-callout=pf2-tip] .callout-content a.external-link:hover {\n  color: rgb(178.3333333333, 0, 255);\n}\n.callout[data-callout=pf2-tip] .callout-content em {\n  color: rgb(51, 51, 51);\n}\n.callout[data-callout=pf2-tip] .callout-content h1 {\n  color: rgb(80, 67, 79);\n}\n.callout[data-callout=pf2-tip] .callout-content h2 {\n  color: rgb(44, 70, 78);\n}\n.callout[data-callout=pf2-tip] .callout-content h3 {\n  color: rgb(51, 51, 51);\n}\n.callout[data-callout=pf2-tip] .callout-content h4 {\n  color: rgb(42, 72, 62);\n}\n.callout[data-callout=pf2-tip] .callout-content h5 {\n  color: rgb(102, 51, 51);\n}\n.callout[data-callout=pf2-tip] .callout-content h6 {\n  color: rgb(36, 36, 36);\n}\n.callout[data-callout=pf2-tip] .callout-content strong {\n  color: rgb(51, 66, 82);\n}\n.callout[data-callout=pf2-tip] .callout-content strong em {\n  color: rgb(51, 58.5, 66.5);\n}\n.callout[data-callout=pf2-tip] .callout-content ol > li::marker,\n.callout[data-callout=pf2-tip] .callout-content ul > li::marker {\n  color: rgb(204, 72, 0);\n}\n.callout[data-callout=pf2-tip] .callout-content p {\n  text-align: justify;\n}\n\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n.callout[data-callout=success-degree] {\n  --callout-icon: lucide-scale;\n}\n.callout[data-callout=success-degree] .callout-title {\n  display: inherit;\n}\n.callout[data-callout=success-degree] .callout-title .callout-icon .svg-icon {\n  display: block;\n  margin-bottom: auto;\n  margin-left: auto;\n  margin-right: auto;\n}\n.callout[data-callout=success-degree] .callout-title .callout-title-inner {\n  display: none;\n}\n.callout[data-callout=success-degree] .callout-content a.internal-link {\n  color: rgb(102, 51, 51);\n}\n.callout[data-callout=success-degree] .callout-content a.internal-link:hover {\n  color: #aa5555;\n}\n.callout[data-callout=success-degree] .callout-content a.internal-link.is-unresolved {\n  color: rgb(0, 73, 97);\n}\n.callout[data-callout=success-degree] .callout-content a.internal-link.is-unresolved:hover {\n  color: rgb(0, 149.7628865979, 199);\n}\n.callout[data-callout=success-degree] .callout-content a.internal-link:is([href*=\"#Actions\"]) {\n  color: transparent;\n}\n.callout[data-callout=success-degree] .callout-content a.internal-link:is([href*=\"#Actions\"]):has(.admonition) {\n  vertical-align: sub;\n}\n.callout[data-callout=success-degree] .callout-content a.external-link {\n  color: rgb(107, 0, 153);\n}\n.callout[data-callout=success-degree] .callout-content a.external-link:hover {\n  color: rgb(178.3333333333, 0, 255);\n}\n.callout[data-callout=success-degree] .callout-content em {\n  color: rgb(51, 51, 51);\n}\n.callout[data-callout=success-degree] .callout-content h1 {\n  color: rgb(80, 67, 79);\n}\n.callout[data-callout=success-degree] .callout-content h2 {\n  color: rgb(44, 70, 78);\n}\n.callout[data-callout=success-degree] .callout-content h3 {\n  color: rgb(51, 51, 51);\n}\n.callout[data-callout=success-degree] .callout-content h4 {\n  color: rgb(42, 72, 62);\n}\n.callout[data-callout=success-degree] .callout-content h5 {\n  color: rgb(102, 51, 51);\n}\n.callout[data-callout=success-degree] .callout-content h6 {\n  color: rgb(36, 36, 36);\n}\n.callout[data-callout=success-degree] .callout-content strong {\n  color: rgb(51, 66, 82);\n}\n.callout[data-callout=success-degree] .callout-content strong em {\n  color: rgb(51, 58.5, 66.5);\n}\n.callout[data-callout=success-degree] .callout-content ol > li::marker,\n.callout[data-callout=success-degree] .callout-content ul > li::marker {\n  color: rgb(204, 72, 0);\n}\n.callout[data-callout=success-degree] .callout-content strong {\n  font-weight: 800;\n}\n\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n.admonition.admonition[data-callout=pf2-note] {\n  --callout-icon: '<svg xmlns=\"http://www.w3.org/2000/svg\"   viewBox=\"0 0 1024 1024\"><path fill=\"currentColor\" d=\"M854.173 764.089H712.398l-1.355-2.419c.501-86.681 34.198-175.368 98.214-245.312 47.831-52.261 83.95-121.041 102.458-189.368l-82.338-49.795 76.414-13.791-86.599-51.529 104.084-17.674c-10.445-78.138-57.588-136.002-154.489-136.002-94.565 18.374-363.959 23.983-503.322 5.922-17.664-5.751-35.459-9.136-52.817-9.731-.129-.035-.267-.068-.394-.102l.219.097c-45.31-1.502-87.613 16.03-116.708 60.236-40.414 61.408 2.616 166.812 97.48 166.812 5.359 0 10.253-.289 14.727-.832v.733h118.467c-17.921 72.21-62.36 152.533-113.828 233.474a564.359 564.359 0 0 0-12.575 20.767l69.657 71.359-112.351 18.818c-65.349 171.651-54.745 363.552 96.403 363.552v-.002l15.634.11v.234h171.175v-.311h451.103c135.99.002 154.656-225.247-37.485-225.247zM452.277 872.408c-.003-.935-.03-1.888-.055-2.836.025.95.052 1.903.055 2.836zm-4.386-35.788c.027.119.047.232.074.351-.027-.117-.047-.231-.074-.347-5.374-23.696-20.779-43.454-40.897-56.175h.007c20.114 12.721 35.516 32.477 40.89 56.171zm4.214 29.647zm-.459-6.386zm-.765-6.682c-.125-.924-.242-1.844-.384-2.781.144.937.261 1.858.384 2.781zm-1.113-7.143c-.144-.814-.269-1.615-.426-2.437.157.822.282 1.624.426 2.437zM236.579 269.87c28.031-23.871-1.146-80.303-34.722-127.243h115.747c19.346 37.343 21.335 80.462 11.471 127.243h-92.496zm89.202 522.253c51.786-17.213 110.468 15.682 122.11 67.013 1.991 8.782 3.269 17.061 3.911 24.882.075-.817.112-1.662.17-2.491-1.291 18.519-6.792 33.936-15.602 46.701h-1.034a76.702 76.702 0 0 1-3.062 4.103l1.614-.707c-14.802 19.359-37.539 32.29-64.57 40.636h-28.104l1.368-.6c-29.614-.008-63.612-2.798-63.612-2.798 43.925-.703 62.862-19.294 70.065-40.598-31.298 11.376-64.654-9.072-75.146-39.264-14.161-40.748 12.502-83.784 51.893-96.877zm126.458 83.452c-.018.919-.027 1.846-.065 2.75.038-.905.045-1.831.065-2.75zm-18.22 55.872z\"></path></svg>';\n}\n.admonition.admonition[data-callout=pf2-note] .admonition-title .admonition-title-inner {\n  display: inline;\n}\n.admonition.admonition[data-callout=pf2-note] .admonition-content a.internal-link {\n  color: rgb(102, 51, 51);\n}\n.admonition.admonition[data-callout=pf2-note] .admonition-content a.internal-link:hover {\n  color: #aa5555;\n}\n.admonition.admonition[data-callout=pf2-note] .admonition-content a.internal-link.is-unresolved {\n  color: rgb(0, 73, 97);\n}\n.admonition.admonition[data-callout=pf2-note] .admonition-content a.internal-link.is-unresolved:hover {\n  color: rgb(0, 149.7628865979, 199);\n}\n.admonition.admonition[data-callout=pf2-note] .admonition-content a.internal-link:is([href*=\"#Actions\"]) {\n  color: transparent;\n}\n.admonition.admonition[data-callout=pf2-note] .admonition-content a.internal-link:is([href*=\"#Actions\"]):has(.admonition) {\n  vertical-align: sub;\n}\n.admonition.admonition[data-callout=pf2-note] .admonition-content a.external-link {\n  color: rgb(107, 0, 153);\n}\n.admonition.admonition[data-callout=pf2-note] .admonition-content a.external-link:hover {\n  color: rgb(178.3333333333, 0, 255);\n}\n.admonition.admonition[data-callout=pf2-note] .admonition-content em {\n  color: rgb(51, 51, 51);\n}\n.admonition.admonition[data-callout=pf2-note] .admonition-content h1 {\n  color: rgb(80, 67, 79);\n}\n.admonition.admonition[data-callout=pf2-note] .admonition-content h2 {\n  color: rgb(44, 70, 78);\n}\n.admonition.admonition[data-callout=pf2-note] .admonition-content h3 {\n  color: rgb(51, 51, 51);\n}\n.admonition.admonition[data-callout=pf2-note] .admonition-content h4 {\n  color: rgb(42, 72, 62);\n}\n.admonition.admonition[data-callout=pf2-note] .admonition-content h5 {\n  color: rgb(102, 51, 51);\n}\n.admonition.admonition[data-callout=pf2-note] .admonition-content h6 {\n  color: rgb(36, 36, 36);\n}\n.admonition.admonition[data-callout=pf2-note] .admonition-content strong {\n  color: rgb(51, 66, 82);\n}\n.admonition.admonition[data-callout=pf2-note] .admonition-content strong em {\n  color: rgb(51, 58.5, 66.5);\n}\n.admonition.admonition[data-callout=pf2-note] .admonition-content ol > li::marker,\n.admonition.admonition[data-callout=pf2-note] .admonition-content ul > li::marker {\n  color: rgb(204, 72, 0);\n}\n.admonition.admonition[data-callout=pf2-note] .admonition-content ul, .admonition.admonition[data-callout=pf2-note] .admonition-content ol, .admonition.admonition[data-callout=pf2-note] .admonition-content li {\n  text-align: left;\n}\n\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n.admonition[data-callout=embed-ability] {\n  --callout-icon: '<svg xmlns=\"http://www.w3.org/2000/svg\"   viewBox=\"0 0 1024 1024\"><path fill=\"currentColor\" d=\"M908.682 797.44c139.17-191.525 122.422-461.107-50.286-633.818-176.21-176.21-453.266-190.09-645.372-41.652-27.817-32.566-54.723-65.058-78.801-95.832L30.954 129.413c30.778 23.828 63.451 50.248 96.266 77.522-151.767 192.241-138.96 471.937 38.483 649.38 172.129 172.129 440.475 189.331 631.878 51.674l40.78 40.778 154.422 38.458-43.596-149.283-40.504-40.502zM660.181 548.939L426.963 315.724l87.314-87.321-63.667-63.672c180.697-9.014 330.398 207.332 209.571 384.207zM446.375 164.971L342.652 268.702a11470.78 11470.78 0 0 1-39.017-43.372c46.748-38.523 95.723-57.103 142.74-60.359zm-212.31 133.097a6682.191 6682.191 0 0 1 42.134 37.09L168.442 442.922l65.666 65.67 82.032-82.037L794.61 905.02C412.147 1162.465-75.257 688.289 234.064 298.069z\"></path></svg>';\n}\n.admonition[data-callout=embed-ability] .admonition-content a.internal-link {\n  color: rgb(102, 51, 51);\n}\n.admonition[data-callout=embed-ability] .admonition-content a.internal-link:hover {\n  color: #aa5555;\n}\n.admonition[data-callout=embed-ability] .admonition-content a.internal-link.is-unresolved {\n  color: rgb(0, 73, 97);\n}\n.admonition[data-callout=embed-ability] .admonition-content a.internal-link.is-unresolved:hover {\n  color: rgb(0, 149.7628865979, 199);\n}\n.admonition[data-callout=embed-ability] .admonition-content a.internal-link:is([href*=\"#Actions\"]) {\n  color: transparent;\n}\n.admonition[data-callout=embed-ability] .admonition-content a.internal-link:is([href*=\"#Actions\"]):has(.admonition) {\n  vertical-align: sub;\n}\n.admonition[data-callout=embed-ability] .admonition-content a.external-link {\n  color: rgb(107, 0, 153);\n}\n.admonition[data-callout=embed-ability] .admonition-content a.external-link:hover {\n  color: rgb(178.3333333333, 0, 255);\n}\n.admonition[data-callout=embed-ability] .admonition-content em {\n  color: rgb(51, 51, 51);\n}\n.admonition[data-callout=embed-ability] .admonition-content h1 {\n  color: rgb(80, 67, 79);\n}\n.admonition[data-callout=embed-ability] .admonition-content h2 {\n  color: rgb(44, 70, 78);\n}\n.admonition[data-callout=embed-ability] .admonition-content h3 {\n  color: rgb(51, 51, 51);\n}\n.admonition[data-callout=embed-ability] .admonition-content h4 {\n  color: rgb(42, 72, 62);\n}\n.admonition[data-callout=embed-ability] .admonition-content h5 {\n  color: rgb(102, 51, 51);\n}\n.admonition[data-callout=embed-ability] .admonition-content h6 {\n  color: rgb(36, 36, 36);\n}\n.admonition[data-callout=embed-ability] .admonition-content strong {\n  color: rgb(51, 66, 82);\n}\n.admonition[data-callout=embed-ability] .admonition-content strong em {\n  color: rgb(51, 58.5, 66.5);\n}\n.admonition[data-callout=embed-ability] .admonition-content ol > li::marker,\n.admonition[data-callout=embed-ability] .admonition-content ul > li::marker {\n  color: rgb(204, 72, 0);\n}\n\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n.admonition[data-callout=embed-action] {\n  --callout-icon: '<svg aria-hidden=\"true\" focusable=\"false\" data-prefix=\"fas\" data-icon=\"campground\" class=\"svg-inline--fa fa-campground fa-w-20\" role=\"img\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 640 512\"><path fill=\"currentColor\" d=\"M624 448h-24.68L359.54 117.75l53.41-73.55c5.19-7.15 3.61-17.16-3.54-22.35l-25.9-18.79c-7.15-5.19-17.15-3.61-22.35 3.55L320 63.3 278.83 6.6c-5.19-7.15-15.2-8.74-22.35-3.55l-25.88 18.8c-7.15 5.19-8.74 15.2-3.54 22.35l53.41 73.55L40.68 448H16c-8.84 0-16 7.16-16 16v32c0 8.84 7.16 16 16 16h608c8.84 0 16-7.16 16-16v-32c0-8.84-7.16-16-16-16zM320 288l116.36 160H203.64L320 288z\"></path></svg>';\n}\n.admonition[data-callout=embed-action] .admonition-content a.internal-link {\n  color: rgb(102, 51, 51);\n}\n.admonition[data-callout=embed-action] .admonition-content a.internal-link:hover {\n  color: #aa5555;\n}\n.admonition[data-callout=embed-action] .admonition-content a.internal-link.is-unresolved {\n  color: rgb(0, 73, 97);\n}\n.admonition[data-callout=embed-action] .admonition-content a.internal-link.is-unresolved:hover {\n  color: rgb(0, 149.7628865979, 199);\n}\n.admonition[data-callout=embed-action] .admonition-content a.internal-link:is([href*=\"#Actions\"]) {\n  color: transparent;\n}\n.admonition[data-callout=embed-action] .admonition-content a.internal-link:is([href*=\"#Actions\"]):has(.admonition) {\n  vertical-align: sub;\n}\n.admonition[data-callout=embed-action] .admonition-content a.external-link {\n  color: rgb(107, 0, 153);\n}\n.admonition[data-callout=embed-action] .admonition-content a.external-link:hover {\n  color: rgb(178.3333333333, 0, 255);\n}\n.admonition[data-callout=embed-action] .admonition-content em {\n  color: rgb(51, 51, 51);\n}\n.admonition[data-callout=embed-action] .admonition-content h1 {\n  color: rgb(80, 67, 79);\n}\n.admonition[data-callout=embed-action] .admonition-content h2 {\n  color: rgb(44, 70, 78);\n}\n.admonition[data-callout=embed-action] .admonition-content h3 {\n  color: rgb(51, 51, 51);\n}\n.admonition[data-callout=embed-action] .admonition-content h4 {\n  color: rgb(42, 72, 62);\n}\n.admonition[data-callout=embed-action] .admonition-content h5 {\n  color: rgb(102, 51, 51);\n}\n.admonition[data-callout=embed-action] .admonition-content h6 {\n  color: rgb(36, 36, 36);\n}\n.admonition[data-callout=embed-action] .admonition-content strong {\n  color: rgb(51, 66, 82);\n}\n.admonition[data-callout=embed-action] .admonition-content strong em {\n  color: rgb(51, 58.5, 66.5);\n}\n.admonition[data-callout=embed-action] .admonition-content ol > li::marker,\n.admonition[data-callout=embed-action] .admonition-content ul > li::marker {\n  color: rgb(204, 72, 0);\n}\n\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n.admonition[data-callout=embed-avatar] {\n  --callout-icon: '<svg aria-hidden=\"true\" focusable=\"false\" data-prefix=\"fas\" data-icon=\"church\" class=\"svg-inline--fa fa-church fa-w-20\" role=\"img\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 640 512\"><path fill=\"currentColor\" d=\"M464.46 246.68L352 179.2V128h48c8.84 0 16-7.16 16-16V80c0-8.84-7.16-16-16-16h-48V16c0-8.84-7.16-16-16-16h-32c-8.84 0-16 7.16-16 16v48h-48c-8.84 0-16 7.16-16 16v32c0 8.84 7.16 16 16 16h48v51.2l-112.46 67.48A31.997 31.997 0 0 0 160 274.12V512h96v-96c0-35.35 28.65-64 64-64s64 28.65 64 64v96h96V274.12c0-11.24-5.9-21.66-15.54-27.44zM0 395.96V496c0 8.84 7.16 16 16 16h112V320L19.39 366.54A32.024 32.024 0 0 0 0 395.96zm620.61-29.42L512 320v192h112c8.84 0 16-7.16 16-16V395.96c0-12.8-7.63-24.37-19.39-29.42z\"></path></svg>';\n}\n.admonition[data-callout=embed-avatar] a.internal-link {\n  color: rgb(100, 2, 59);\n}\n.admonition[data-callout=embed-avatar] a.internal-link:hover {\n  color: rgb(50, 1, 29.5);\n}\n.admonition[data-callout=embed-avatar] a.internal-link:is([href*=\"#Actions\"]) {\n  color: transparent;\n}\n.admonition[data-callout=embed-avatar] .admonition-title .admonition-title-content {\n  font-family: Garamond, \"Baskerville Old Face\", \"Hoefler Text\", \"Times New Roman\", serif;\n  text-align: center;\n}\n.admonition[data-callout=embed-avatar] .admonition-content a.internal-link.is-unresolved {\n  color: rgb(0, 73, 97);\n}\n.admonition[data-callout=embed-avatar] .admonition-content a.internal-link.is-unresolved:hover {\n  color: rgb(0, 149.7628865979, 199);\n}\n.admonition[data-callout=embed-avatar] .admonition-content a.external-link {\n  color: rgb(107, 0, 153);\n}\n.admonition[data-callout=embed-avatar] .admonition-content a.external-link:hover {\n  color: rgb(35.6666666667, 0, 51);\n}\n.admonition[data-callout=embed-avatar] .admonition-content em {\n  color: rgb(51, 51, 51);\n}\n.admonition[data-callout=embed-avatar] .admonition-content h1 {\n  color: rgb(102, 51, 51);\n}\n.admonition[data-callout=embed-avatar] .admonition-content h2 {\n  color: rgb(44, 70, 78);\n}\n.admonition[data-callout=embed-avatar] .admonition-content h3 {\n  color: rgb(80, 67, 79);\n}\n.admonition[data-callout=embed-avatar] .admonition-content h4 {\n  color: rgb(42, 72, 62);\n}\n.admonition[data-callout=embed-avatar] .admonition-content h5 {\n  color: rgb(51, 51, 51);\n}\n.admonition[data-callout=embed-avatar] .admonition-content h6 {\n  color: rgb(36, 36, 36);\n}\n.admonition[data-callout=embed-avatar] .admonition-content strong {\n  color: rgb(51, 66, 82);\n}\n.admonition[data-callout=embed-avatar] .admonition-content strong em {\n  color: rgb(51, 58.5, 66.5);\n}\n.admonition[data-callout=embed-avatar] .admonition-content ol > li::marker,\n.admonition[data-callout=embed-avatar] .admonition-content ul > li::marker {\n  color: rgb(19, 138, 6);\n}\n\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n.admonition[data-callout=embed-disease] {\n  --callout-icon: '<svg aria-hidden=\"true\" focusable=\"false\" data-prefix=\"fas\" data-icon=\"head-side-virus\" class=\"svg-inline--fa fa-head-side-virus fa-w-16\" role=\"img\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\"><path fill=\"currentColor\" d=\"M272,240a16,16,0,1,0,16,16A16,16,0,0,0,272,240Zm-64-64a16,16,0,1,0,16,16A16,16,0,0,0,208,176Zm301.2,99c-20.93-47.12-48.43-151.73-73.07-186.75A207.9,207.9,0,0,0,266.09,0H192C86,0,0,86,0,192A191.23,191.23,0,0,0,64,334.81V512H320V448h64a64,64,0,0,0,64-64V320H480A32,32,0,0,0,509.2,275ZM368,240H355.88c-28.51,0-42.79,34.47-22.63,54.63l8.58,8.57a16,16,0,1,1-22.63,22.63l-8.57-8.58C290.47,297.09,256,311.37,256,339.88V352a16,16,0,0,1-32,0V339.88c0-28.51-34.47-42.79-54.63-22.63l-8.57,8.58a16,16,0,0,1-22.63-22.63l8.58-8.57c20.16-20.16,5.88-54.63-22.63-54.63H112a16,16,0,0,1,0-32h12.12c28.51,0,42.79-34.47,22.63-54.63l-8.58-8.57a16,16,0,0,1,22.63-22.63l8.57,8.58c20.16,20.16,54.63,5.88,54.63-22.63V96a16,16,0,0,1,32,0v12.12c0,28.51,34.47,42.79,54.63,22.63l8.57-8.58a16,16,0,0,1,22.63,22.63l-8.58,8.57C313.09,173.53,327.37,208,355.88,208H368a16,16,0,0,1,0,32Z\"></path></svg>';\n  color: rgb(232, 232, 232);\n  font-family: \"Abril Fatface\", \"Cinzel Decorative\", \"Creepster\", \"Henny Penny\", \"IM Fell English\", \"Kaushan Script\", \"Lobster\", \"Montserrat Alternates\", \"Nosifer\", \"Permanent Marker\", \"Playfair Display\", \"Righteous\", \"Roboto Slab\", \"Special Elite\", \"UnifrakturCook\", cursive;\n}\n.admonition[data-callout=embed-disease] .admonition-content a.internal-link {\n  color: rgb(255, 219, 88);\n}\n.admonition[data-callout=embed-disease] .admonition-content a.internal-link:hover {\n  color: rgb(241, 189.0479041916, 0);\n}\n.admonition[data-callout=embed-disease] .admonition-content a.internal-link.is-unresolved {\n  color: rgb(179.25, 208.5, 59.5);\n}\n.admonition[data-callout=embed-disease] .admonition-content a.internal-link.is-unresolved:hover {\n  color: rgb(114.0392561983, 134.1033057851, 31.8966942149);\n}\n.admonition[data-callout=embed-disease] .admonition-content a.external-link {\n  color: rgb(var(--callout-color));\n  filter: invert(100%);\n}\n.admonition[data-callout=embed-disease] .admonition-content a.external-link:hover {\n  filter: invert(100%) brightness(1.2);\n}\n.admonition[data-callout=embed-disease] .admonition-content em {\n  color: rgb(154, 205, 50);\n}\n.admonition[data-callout=embed-disease] .admonition-content h1, .admonition[data-callout=embed-disease] .admonition-content h2, .admonition[data-callout=embed-disease] .admonition-content h3, .admonition[data-callout=embed-disease] .admonition-content h4, .admonition[data-callout=embed-disease] .admonition-content h5, .admonition[data-callout=embed-disease] .admonition-content h6 {\n  color: rgb(232, 232, 232);\n}\n.admonition[data-callout=embed-disease] .admonition-content p {\n  color: rgb(232, 232, 232);\n}\n.admonition[data-callout=embed-disease] .admonition-content strong {\n  color: rgb(255, 143, 143);\n}\n.admonition[data-callout=embed-disease] .admonition-content strong em {\n  color: rgb(204.5, 174, 96.5);\n}\n.admonition[data-callout=embed-disease] .admonition-content ol > li::marker,\n.admonition[data-callout=embed-disease] .admonition-content ul > li::marker {\n  color: rgb(204, 72, 0);\n}\n.admonition[data-callout=embed-disease] .admonition-content ul {\n  margin-block-end: unset;\n  margin-block-start: unset;\n  padding: 0.5em;\n}\n\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n.admonition[data-callout=embed-feat] {\n  --callout-icon: '<svg aria-hidden=\"true\" focusable=\"false\" data-prefix=\"fas\" data-icon=\"award\" class=\"svg-inline--fa fa-award fa-w-12\" role=\"img\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 384 512\"><path fill=\"currentColor\" d=\"M97.12 362.63c-8.69-8.69-4.16-6.24-25.12-11.85-9.51-2.55-17.87-7.45-25.43-13.32L1.2 448.7c-4.39 10.77 3.81 22.47 15.43 22.03l52.69-2.01L105.56 507c8 8.44 22.04 5.81 26.43-4.96l52.05-127.62c-10.84 6.04-22.87 9.58-35.31 9.58-19.5 0-37.82-7.59-51.61-21.37zM382.8 448.7l-45.37-111.24c-7.56 5.88-15.92 10.77-25.43 13.32-21.07 5.64-16.45 3.18-25.12 11.85-13.79 13.78-32.12 21.37-51.62 21.37-12.44 0-24.47-3.55-35.31-9.58L252 502.04c4.39 10.77 18.44 13.4 26.43 4.96l36.25-38.28 52.69 2.01c11.62.44 19.82-11.27 15.43-22.03zM263 340c15.28-15.55 17.03-14.21 38.79-20.14 13.89-3.79 24.75-14.84 28.47-28.98 7.48-28.4 5.54-24.97 25.95-45.75 10.17-10.35 14.14-25.44 10.42-39.58-7.47-28.38-7.48-24.42 0-52.83 3.72-14.14-.25-29.23-10.42-39.58-20.41-20.78-18.47-17.36-25.95-45.75-3.72-14.14-14.58-25.19-28.47-28.98-27.88-7.61-24.52-5.62-44.95-26.41-10.17-10.35-25-14.4-38.89-10.61-27.87 7.6-23.98 7.61-51.9 0-13.89-3.79-28.72.25-38.89 10.61-20.41 20.78-17.05 18.8-44.94 26.41-13.89 3.79-24.75 14.84-28.47 28.98-7.47 28.39-5.54 24.97-25.95 45.75-10.17 10.35-14.15 25.44-10.42 39.58 7.47 28.36 7.48 24.4 0 52.82-3.72 14.14.25 29.23 10.42 39.59 20.41 20.78 18.47 17.35 25.95 45.75 3.72 14.14 14.58 25.19 28.47 28.98C104.6 325.96 106.27 325 121 340c13.23 13.47 33.84 15.88 49.74 5.82a39.676 39.676 0 0 1 42.53 0c15.89 10.06 36.5 7.65 49.73-5.82zM97.66 175.96c0-53.03 42.24-96.02 94.34-96.02s94.34 42.99 94.34 96.02-42.24 96.02-94.34 96.02-94.34-42.99-94.34-96.02z\"></path></svg>';\n}\n.admonition[data-callout=embed-feat] .admonition-content a.internal-link {\n  color: rgb(102, 51, 51);\n}\n.admonition[data-callout=embed-feat] .admonition-content a.internal-link:hover {\n  color: #aa5555;\n}\n.admonition[data-callout=embed-feat] .admonition-content a.internal-link.is-unresolved {\n  color: rgb(0, 73, 97);\n}\n.admonition[data-callout=embed-feat] .admonition-content a.internal-link.is-unresolved:hover {\n  color: rgb(0, 149.7628865979, 199);\n}\n.admonition[data-callout=embed-feat] .admonition-content a.internal-link:is([href*=\"#Actions\"]) {\n  color: transparent;\n}\n.admonition[data-callout=embed-feat] .admonition-content a.internal-link:is([href*=\"#Actions\"]):has(.admonition) {\n  vertical-align: sub;\n}\n.admonition[data-callout=embed-feat] .admonition-content a.external-link {\n  color: rgb(107, 0, 153);\n}\n.admonition[data-callout=embed-feat] .admonition-content a.external-link:hover {\n  color: rgb(178.3333333333, 0, 255);\n}\n.admonition[data-callout=embed-feat] .admonition-content em {\n  color: rgb(51, 51, 51);\n}\n.admonition[data-callout=embed-feat] .admonition-content h1 {\n  color: rgb(80, 67, 79);\n}\n.admonition[data-callout=embed-feat] .admonition-content h2 {\n  color: rgb(44, 70, 78);\n}\n.admonition[data-callout=embed-feat] .admonition-content h3 {\n  color: rgb(51, 51, 51);\n}\n.admonition[data-callout=embed-feat] .admonition-content h4 {\n  color: rgb(42, 72, 62);\n}\n.admonition[data-callout=embed-feat] .admonition-content h5 {\n  color: rgb(102, 51, 51);\n}\n.admonition[data-callout=embed-feat] .admonition-content h6 {\n  color: rgb(36, 36, 36);\n}\n.admonition[data-callout=embed-feat] .admonition-content strong {\n  color: rgb(51, 66, 82);\n}\n.admonition[data-callout=embed-feat] .admonition-content strong em {\n  color: rgb(51, 58.5, 66.5);\n}\n.admonition[data-callout=embed-feat] .admonition-content ol > li::marker,\n.admonition[data-callout=embed-feat] .admonition-content ul > li::marker {\n  color: rgb(204, 72, 0);\n}\n\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n.admonition[data-callout=embed-item] {\n  --callout-icon: '<svg aria-hidden=\"true\" focusable=\"false\" data-prefix=\"fas\" data-icon=\"coins\" class=\"svg-inline--fa fa-coins fa-w-16\" role=\"img\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\"><path fill=\"currentColor\" d=\"M0 405.3V448c0 35.3 86 64 192 64s192-28.7 192-64v-42.7C342.7 434.4 267.2 448 192 448S41.3 434.4 0 405.3zM320 128c106 0 192-28.7 192-64S426 0 320 0 128 28.7 128 64s86 64 192 64zM0 300.4V352c0 35.3 86 64 192 64s192-28.7 192-64v-51.6c-41.3 34-116.9 51.6-192 51.6S41.3 334.4 0 300.4zm416 11c57.3-11.1 96-31.7 96-55.4v-42.7c-23.2 16.4-57.3 27.6-96 34.5v63.6zM192 160C86 160 0 195.8 0 240s86 80 192 80 192-35.8 192-80-86-80-192-80zm219.3 56.3c60-10.8 100.7-32 100.7-56.3v-42.7c-35.5 25.1-96.5 38.6-160.7 41.8 29.5 14.3 51.2 33.5 60 57.2z\"></path></svg>';\n}\n.admonition[data-callout=embed-item] .admonition-content a.internal-link {\n  color: rgb(102, 51, 51);\n}\n.admonition[data-callout=embed-item] .admonition-content a.internal-link:hover {\n  color: #aa5555;\n}\n.admonition[data-callout=embed-item] .admonition-content a.internal-link.is-unresolved {\n  color: rgb(0, 73, 97);\n}\n.admonition[data-callout=embed-item] .admonition-content a.internal-link.is-unresolved:hover {\n  color: rgb(0, 149.7628865979, 199);\n}\n.admonition[data-callout=embed-item] .admonition-content a.internal-link:is([href*=\"#Actions\"]) {\n  color: transparent;\n}\n.admonition[data-callout=embed-item] .admonition-content a.internal-link:is([href*=\"#Actions\"]):has(.admonition) {\n  vertical-align: sub;\n}\n.admonition[data-callout=embed-item] .admonition-content a.external-link {\n  color: rgb(107, 0, 153);\n}\n.admonition[data-callout=embed-item] .admonition-content a.external-link:hover {\n  color: rgb(178.3333333333, 0, 255);\n}\n.admonition[data-callout=embed-item] .admonition-content em {\n  color: rgb(51, 51, 51);\n}\n.admonition[data-callout=embed-item] .admonition-content h1 {\n  color: rgb(80, 67, 79);\n}\n.admonition[data-callout=embed-item] .admonition-content h2 {\n  color: rgb(44, 70, 78);\n}\n.admonition[data-callout=embed-item] .admonition-content h3 {\n  color: rgb(51, 51, 51);\n}\n.admonition[data-callout=embed-item] .admonition-content h4 {\n  color: rgb(42, 72, 62);\n}\n.admonition[data-callout=embed-item] .admonition-content h5 {\n  color: rgb(102, 51, 51);\n}\n.admonition[data-callout=embed-item] .admonition-content h6 {\n  color: rgb(36, 36, 36);\n}\n.admonition[data-callout=embed-item] .admonition-content strong {\n  color: rgb(51, 66, 82);\n}\n.admonition[data-callout=embed-item] .admonition-content strong em {\n  color: rgb(51, 58.5, 66.5);\n}\n.admonition[data-callout=embed-item] .admonition-content ol > li::marker,\n.admonition[data-callout=embed-item] .admonition-content ul > li::marker {\n  color: rgb(204, 72, 0);\n}\n\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n.admonition[data-callout=embed-ritual] {\n  --callout-icon: '<svg aria-hidden=\"true\" focusable=\"false\" data-prefix=\"fas\" data-icon=\"pray\" class=\"svg-inline--fa fa-pray fa-w-12\" role=\"img\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 384 512\"><path fill=\"currentColor\" d=\"M256 128c35.35 0 64-28.65 64-64S291.35 0 256 0s-64 28.65-64 64 28.65 64 64 64zm-30.63 169.75c14.06 16.72 39 19.09 55.97 5.22l88-72.02c17.09-13.98 19.59-39.19 5.62-56.28-13.97-17.11-39.19-19.59-56.31-5.62l-57.44 47-38.91-46.31c-15.44-18.39-39.22-27.92-64-25.33-24.19 2.48-45.25 16.27-56.37 36.92l-49.37 92.03c-23.4 43.64-8.69 96.37 34.19 123.75L131.56 432H40c-22.09 0-40 17.91-40 40s17.91 40 40 40h208c34.08 0 53.77-42.79 28.28-68.28L166.42 333.86l34.8-64.87 24.15 28.76z\"></path></svg>';\n}\n.admonition[data-callout=embed-ritual] .admonition-content a.internal-link {\n  color: rgb(102, 51, 51);\n}\n.admonition[data-callout=embed-ritual] .admonition-content a.internal-link:hover {\n  color: #aa5555;\n}\n.admonition[data-callout=embed-ritual] .admonition-content a.internal-link.is-unresolved {\n  color: rgb(0, 73, 97);\n}\n.admonition[data-callout=embed-ritual] .admonition-content a.internal-link.is-unresolved:hover {\n  color: rgb(0, 149.7628865979, 199);\n}\n.admonition[data-callout=embed-ritual] .admonition-content a.internal-link:is([href*=\"#Actions\"]) {\n  color: transparent;\n}\n.admonition[data-callout=embed-ritual] .admonition-content a.internal-link:is([href*=\"#Actions\"]):has(.admonition) {\n  vertical-align: sub;\n}\n.admonition[data-callout=embed-ritual] .admonition-content a.external-link {\n  color: rgb(107, 0, 153);\n}\n.admonition[data-callout=embed-ritual] .admonition-content a.external-link:hover {\n  color: rgb(178.3333333333, 0, 255);\n}\n.admonition[data-callout=embed-ritual] .admonition-content em {\n  color: rgb(51, 51, 51);\n}\n.admonition[data-callout=embed-ritual] .admonition-content h1 {\n  color: rgb(80, 67, 79);\n}\n.admonition[data-callout=embed-ritual] .admonition-content h2 {\n  color: rgb(44, 70, 78);\n}\n.admonition[data-callout=embed-ritual] .admonition-content h3 {\n  color: rgb(51, 51, 51);\n}\n.admonition[data-callout=embed-ritual] .admonition-content h4 {\n  color: rgb(42, 72, 62);\n}\n.admonition[data-callout=embed-ritual] .admonition-content h5 {\n  color: rgb(102, 51, 51);\n}\n.admonition[data-callout=embed-ritual] .admonition-content h6 {\n  color: rgb(36, 36, 36);\n}\n.admonition[data-callout=embed-ritual] .admonition-content strong {\n  color: rgb(51, 66, 82);\n}\n.admonition[data-callout=embed-ritual] .admonition-content strong em {\n  color: rgb(51, 58.5, 66.5);\n}\n.admonition[data-callout=embed-ritual] .admonition-content ol > li::marker,\n.admonition[data-callout=embed-ritual] .admonition-content ul > li::marker {\n  color: rgb(204, 72, 0);\n}\n\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n.admonition[data-callout=inline-affliction] {\n  --callout-icon: '<svg aria-hidden=\"true\" focusable=\"false\" data-prefix=\"fas\" data-icon=\"virus-slash\" class=\"svg-inline--fa fa-virus-slash fa-w-20\" role=\"img\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 640 512\"><path fill=\"currentColor\" d=\"M114,227.6H92.4C76.7,227.6,64,240.3,64,256s12.7,28.4,28.4,28.4H114c50.7,0,76.1,61.3,40.2,97.1L139,396.8 c-11.5,10.7-12.2,28.7-1.6,40.2s28.7,12.2,40.2,1.6c0.5-0.5,1.1-1,1.6-1.6l15.2-15.2c35.8-35.8,97.1-10.5,97.1,40.2v21.5 c0,15.7,12.8,28.4,28.5,28.4c15.7,0,28.4-12.7,28.4-28.4V462c0-26.6,17-45.9,38.2-53.4l-244.5-189 C133.7,224.7,123.9,227.5,114,227.6z M617,505.8l19.6-25.3c5.4-7,4.2-17-2.8-22.5L470.6,332c4.2-25.4,24.9-47.5,55.4-47.5h21.5 c15.7,0,28.4-12.7,28.4-28.4s-12.7-28.4-28.4-28.4H526c-50.7,0-76.1-61.3-40.2-97.1l15.2-15.3c10.7-11.5,10-29.5-1.6-40.2 c-10.9-10.1-27.7-10.1-38.6,0l-15.2,15.2c-35.8,35.8-97.1,10.5-97.1-40.2V28.5C348.4,12.7,335.7,0,320,0 c-15.7,0-28.4,12.7-28.4,28.4V50c0,50.7-61.3,76.1-97.1,40.2L179.2,75c-11.1-11.1-29.4-10.6-40.5,0.5L45.5,3.4 c-7-5.4-17-4.2-22.5,2.8L3.4,31.5c-5.4,7-4.2,17,2.8,22.5l588.4,454.7C601.5,514.1,611.6,512.8,617,505.8z M335.4,227.5l-62.9-48.6 c4.9-1.8,10.2-2.8,15.4-2.9c26.5,0,48,21.5,48,48C336,225.2,335.5,226.3,335.4,227.5z\"></path></svg>';\n}\n.admonition[data-callout=inline-affliction] .admonition-title-content {\n  color: rgb(80, 67, 79);\n}\n.admonition[data-callout=inline-affliction] .admonition-content a.internal-link {\n  color: rgb(102, 51, 51);\n}\n.admonition[data-callout=inline-affliction] .admonition-content a.internal-link:hover {\n  color: #aa5555;\n}\n.admonition[data-callout=inline-affliction] .admonition-content a.internal-link.is-unresolved {\n  color: rgb(0, 73, 97);\n}\n.admonition[data-callout=inline-affliction] .admonition-content a.internal-link.is-unresolved:hover {\n  color: rgb(0, 149.7628865979, 199);\n}\n.admonition[data-callout=inline-affliction] .admonition-content a.internal-link:is([href*=\"#Actions\"]) {\n  color: transparent;\n}\n.admonition[data-callout=inline-affliction] .admonition-content a.internal-link:is([href*=\"#Actions\"]):has(.admonition) {\n  vertical-align: sub;\n}\n.admonition[data-callout=inline-affliction] .admonition-content a.external-link {\n  color: rgb(107, 0, 153);\n}\n.admonition[data-callout=inline-affliction] .admonition-content a.external-link:hover {\n  color: rgb(178.3333333333, 0, 255);\n}\n.admonition[data-callout=inline-affliction] .admonition-content em {\n  color: rgb(51, 51, 51);\n}\n.admonition[data-callout=inline-affliction] .admonition-content h1 {\n  color: rgb(80, 67, 79);\n}\n.admonition[data-callout=inline-affliction] .admonition-content h2 {\n  color: rgb(44, 70, 78);\n}\n.admonition[data-callout=inline-affliction] .admonition-content h3 {\n  color: rgb(51, 51, 51);\n}\n.admonition[data-callout=inline-affliction] .admonition-content h4 {\n  color: rgb(42, 72, 62);\n}\n.admonition[data-callout=inline-affliction] .admonition-content h5 {\n  color: rgb(102, 51, 51);\n}\n.admonition[data-callout=inline-affliction] .admonition-content h6 {\n  color: rgb(36, 36, 36);\n}\n.admonition[data-callout=inline-affliction] .admonition-content strong {\n  color: rgb(51, 66, 82);\n}\n.admonition[data-callout=inline-affliction] .admonition-content strong em {\n  color: rgb(51, 58.5, 66.5);\n}\n.admonition[data-callout=inline-affliction] .admonition-content ol > li::marker,\n.admonition[data-callout=inline-affliction] .admonition-content ul > li::marker {\n  color: rgb(204, 72, 0);\n}\n\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n.admonition[data-callout=inline-attack] {\n  --callout-icon: '<svg xmlns=\"http://www.w3.org/2000/svg\"   viewBox=\"0 0 1024 1024\"><path fill=\"currentColor\" d=\"M746.013 346.338c46.494-110.134 130.621 19.244 105.629 112.518-19.444 72.579-84.175 117.611-176.313 58.351l.002-.003c-211.764-136.2-156.573-202.882-263.845-314.147 36.755 155.341-72.25 56.868-170.627-68.533 28.348-16.96 60.092-28.884 94.211-34.921l220.269 148.443 41.1-86.039a516.489 516.489 0 0 1 17.294 11.65C733.437 257.936 719.977 58.832 756.047 17.728l-735.76-1.517 3.623 719.657c78.285 54.695 209.606-14.959 156.548-102.913a642.424 642.424 0 0 1-10.895-18.781l60.214-35.715-123.371-219.527c2.806-33.01 10.277-63.857 21.748-91.975 143.591 147.62 367.783 380.048 166.483 334.739 61.101 57.174 274.614 99.542 298.674 195.863 19.974 79.966-50.973 143.517-111.372 143.517-81.1 0-165.35-64.925-89.556-140.074-48.131 8.002-71.065 41.941-72.331 80.371s19.341 81.427 58.557 107.924h605.069V339.45c-14.114-118.729-241.943-168.205-237.665 6.887zM20.288 16.21v-.005z\"></path></svg>';\n}\n.admonition[data-callout=inline-attack] .admonition-content a.internal-link {\n  color: rgb(102, 51, 51);\n}\n.admonition[data-callout=inline-attack] .admonition-content a.internal-link:hover {\n  color: #aa5555;\n}\n.admonition[data-callout=inline-attack] .admonition-content a.internal-link.is-unresolved {\n  color: rgb(0, 73, 97);\n}\n.admonition[data-callout=inline-attack] .admonition-content a.internal-link.is-unresolved:hover {\n  color: rgb(0, 149.7628865979, 199);\n}\n.admonition[data-callout=inline-attack] .admonition-content a.internal-link:is([href*=\"#Actions\"]) {\n  color: transparent;\n}\n.admonition[data-callout=inline-attack] .admonition-content a.internal-link:is([href*=\"#Actions\"]):has(.admonition) {\n  vertical-align: sub;\n}\n.admonition[data-callout=inline-attack] .admonition-content a.external-link {\n  color: rgb(107, 0, 153);\n}\n.admonition[data-callout=inline-attack] .admonition-content a.external-link:hover {\n  color: rgb(178.3333333333, 0, 255);\n}\n.admonition[data-callout=inline-attack] .admonition-content em {\n  color: rgb(51, 51, 51);\n}\n.admonition[data-callout=inline-attack] .admonition-content h1 {\n  color: rgb(80, 67, 79);\n}\n.admonition[data-callout=inline-attack] .admonition-content h2 {\n  color: rgb(44, 70, 78);\n}\n.admonition[data-callout=inline-attack] .admonition-content h3 {\n  color: rgb(51, 51, 51);\n}\n.admonition[data-callout=inline-attack] .admonition-content h4 {\n  color: rgb(42, 72, 62);\n}\n.admonition[data-callout=inline-attack] .admonition-content h5 {\n  color: rgb(102, 51, 51);\n}\n.admonition[data-callout=inline-attack] .admonition-content h6 {\n  color: rgb(36, 36, 36);\n}\n.admonition[data-callout=inline-attack] .admonition-content strong {\n  color: rgb(51, 66, 82);\n}\n.admonition[data-callout=inline-attack] .admonition-content strong em {\n  color: rgb(51, 58.5, 66.5);\n}\n.admonition[data-callout=inline-attack] .admonition-content ol > li::marker,\n.admonition[data-callout=inline-attack] .admonition-content ul > li::marker {\n  color: rgb(204, 72, 0);\n}\n\n/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n.admonition[data-callout=pf2-summary] {\n  --callout-icon: '<svg xmlns=\"http://www.w3.org/2000/svg\"   viewBox=\"0 0 1024 1024\"><path fill=\"currentColor\" d=\"M854.173 764.089H712.398l-1.355-2.419c.501-86.681 34.198-175.368 98.214-245.312 47.831-52.261 83.95-121.041 102.458-189.368l-82.338-49.795 76.414-13.791-86.599-51.529 104.084-17.674c-10.445-78.138-57.588-136.002-154.489-136.002-94.565 18.374-363.959 23.983-503.322 5.922-17.664-5.751-35.459-9.136-52.817-9.731-.129-.035-.267-.068-.394-.102l.219.097c-45.31-1.502-87.613 16.03-116.708 60.236-40.414 61.408 2.616 166.812 97.48 166.812 5.359 0 10.253-.289 14.727-.832v.733h118.467c-17.921 72.21-62.36 152.533-113.828 233.474a564.359 564.359 0 0 0-12.575 20.767l69.657 71.359-112.351 18.818c-65.349 171.651-54.745 363.552 96.403 363.552v-.002l15.634.11v.234h171.175v-.311h451.103c135.99.002 154.656-225.247-37.485-225.247zM452.277 872.408c-.003-.935-.03-1.888-.055-2.836.025.95.052 1.903.055 2.836zm-4.386-35.788c.027.119.047.232.074.351-.027-.117-.047-.231-.074-.347-5.374-23.696-20.779-43.454-40.897-56.175h.007c20.114 12.721 35.516 32.477 40.89 56.171zm4.214 29.647zm-.459-6.386zm-.765-6.682c-.125-.924-.242-1.844-.384-2.781.144.937.261 1.858.384 2.781zm-1.113-7.143c-.144-.814-.269-1.615-.426-2.437.157.822.282 1.624.426 2.437zM236.579 269.87c28.031-23.871-1.146-80.303-34.722-127.243h115.747c19.346 37.343 21.335 80.462 11.471 127.243h-92.496zm89.202 522.253c51.786-17.213 110.468 15.682 122.11 67.013 1.991 8.782 3.269 17.061 3.911 24.882.075-.817.112-1.662.17-2.491-1.291 18.519-6.792 33.936-15.602 46.701h-1.034a76.702 76.702 0 0 1-3.062 4.103l1.614-.707c-14.802 19.359-37.539 32.29-64.57 40.636h-28.104l1.368-.6c-29.614-.008-63.612-2.798-63.612-2.798 43.925-.703 62.862-19.294 70.065-40.598-31.298 11.376-64.654-9.072-75.146-39.264-14.161-40.748 12.502-83.784 51.893-96.877zm126.458 83.452c-.018.919-.027 1.846-.065 2.75.038-.905.045-1.831.065-2.75zm-18.22 55.872z\"></path></svg>';\n}\n.admonition[data-callout=pf2-summary] .admonition-content a.internal-link {\n  color: rgb(102, 51, 51);\n}\n.admonition[data-callout=pf2-summary] .admonition-content a.internal-link:hover {\n  color: #aa5555;\n}\n.admonition[data-callout=pf2-summary] .admonition-content a.internal-link.is-unresolved {\n  color: rgb(0, 73, 97);\n}\n.admonition[data-callout=pf2-summary] .admonition-content a.internal-link.is-unresolved:hover {\n  color: rgb(0, 149.7628865979, 199);\n}\n.admonition[data-callout=pf2-summary] .admonition-content a.internal-link:is([href*=\"#Actions\"]) {\n  color: transparent;\n}\n.admonition[data-callout=pf2-summary] .admonition-content a.internal-link:is([href*=\"#Actions\"]):has(.admonition) {\n  vertical-align: sub;\n}\n.admonition[data-callout=pf2-summary] .admonition-content a.external-link {\n  color: rgb(107, 0, 153);\n}\n.admonition[data-callout=pf2-summary] .admonition-content a.external-link:hover {\n  color: rgb(178.3333333333, 0, 255);\n}\n.admonition[data-callout=pf2-summary] .admonition-content em {\n  color: rgb(51, 51, 51);\n}\n.admonition[data-callout=pf2-summary] .admonition-content h1 {\n  color: rgb(80, 67, 79);\n}\n.admonition[data-callout=pf2-summary] .admonition-content h2 {\n  color: rgb(44, 70, 78);\n}\n.admonition[data-callout=pf2-summary] .admonition-content h3 {\n  color: rgb(51, 51, 51);\n}\n.admonition[data-callout=pf2-summary] .admonition-content h4 {\n  color: rgb(42, 72, 62);\n}\n.admonition[data-callout=pf2-summary] .admonition-content h5 {\n  color: rgb(102, 51, 51);\n}\n.admonition[data-callout=pf2-summary] .admonition-content h6 {\n  color: rgb(36, 36, 36);\n}\n.admonition[data-callout=pf2-summary] .admonition-content strong {\n  color: rgb(51, 66, 82);\n}\n.admonition[data-callout=pf2-summary] .admonition-content strong em {\n  color: rgb(51, 58.5, 66.5);\n}\n.admonition[data-callout=pf2-summary] .admonition-content ol > li::marker,\n.admonition[data-callout=pf2-summary] .admonition-content ul > li::marker {\n  color: rgb(204, 72, 0);\n}\n\nbody .callout[data-callout=statblock-pf2e] {\n  background-color: rgb(246, 244, 242);\n  border: none;\n  --callout-color: var(--statblock-pf2e);\n  color: rgb(0, 0, 0);\n  font-family: sans-serif;\n  font-size: 16px;\n  font-weight: 400;\n  line-height: 1.3em;\n  margin: 1rem 0;\n  min-width: 50%;\n  mix-blend-mode: normal;\n  padding: 0 0.25em;\n}\n.theme-dark body .callout[data-callout=statblock-pf2e] {\n  --statblock-pf2e: 201, 60, 60;\n}\n.theme-light body .callout[data-callout=statblock-pf2e] {\n  --statblock-pf2e: 201, 60, 60;\n}\nbody .callout[data-callout=statblock-pf2e] strong {\n  color: rgb(0, 0, 0);\n}\nbody .callout[data-callout=statblock-pf2e] em {\n  color: rgb(0, 0, 0);\n}\nbody .callout[data-callout=statblock-pf2e] strong + em {\n  color: rgb(0, 0, 0);\n}\nbody .callout[data-callout=statblock-pf2e] .callout-title {\n  background-color: rgb(246, 244, 242);\n  border: none;\n  border-radius: 0;\n  color: rgb(0, 0, 0);\n  display: flex;\n  font-family: \"Oswald\", sans-serif;\n  font-size: inherit;\n  gap: 0;\n  line-height: 1.3;\n  margin-bottom: 0;\n  padding: 0.25em 0 0;\n}\nbody .callout[data-callout=statblock-pf2e] .callout-title .callout-icon {\n  display: none;\n}\nbody .callout[data-callout=statblock-pf2e] .callout-title .callout-title-inner {\n  color: rgb(0, 0, 0);\n  flex: 1;\n  font-size: 1.35em;\n  font-weight: 700;\n  line-height: 1;\n  margin-bottom: 0;\n  margin-left: 0.25em;\n  padding-bottom: 0;\n  position: relative;\n  text-align: left;\n  text-transform: uppercase;\n}\nbody .callout[data-callout=statblock-pf2e] img[src$=\"#token\"],\nbody .callout[data-callout=statblock-pf2e] div[src$=\"#token\"] {\n  float: right;\n  margin-left: 0.3125em;\n  width: 9.375em;\n}\nbody .callout[data-callout=statblock-pf2e] .callout-content {\n  background-color: rgb(246, 244, 242);\n  margin-top: 0;\n  padding-left: 0.25em;\n  padding-right: 0.25em;\n  padding-top: 0;\n}\nbody .callout[data-callout=statblock-pf2e] .callout-content a {\n  color: rgb(51, 122, 183);\n  font-weight: 700;\n  text-decoration: none;\n}\nbody .callout[data-callout=statblock-pf2e] .callout-content a.external-link {\n  background-image: none;\n  background-size: 0;\n  color: rgb(150, 122, 222);\n  padding-right: 0;\n}\nbody .callout[data-callout=statblock-pf2e] .callout-content a.external-link::after {\n  display: none;\n}\nbody .callout[data-callout=statblock-pf2e] .callout-content .internal-link.is-unresolved::after {\n  display: none;\n}\nbody .callout[data-callout=statblock-pf2e] .callout-content blockquote {\n  background-color: rgb(246, 244, 242);\n  border: none;\n  color: rgb(0, 0, 0);\n  margin-inline-end: 1em;\n  margin-inline-start: 2em;\n  padding: 0;\n}\nbody .callout[data-callout=statblock-pf2e] .callout-content > p {\n  margin-block-start: 0.5em;\n}\nbody .callout[data-callout=statblock-pf2e] .callout-content li {\n  line-height: 1.2em;\n  margin-block-start: 0.5em;\n}\nbody .callout[data-callout=statblock-pf2e] .callout-content > hr {\n  border-color: rgb(0, 0, 0);\n  border-top: 1px solid;\n  height: 1px;\n  margin: 0;\n  width: 100%;\n}\nbody .callout[data-callout=statblock-pf2e] .callout-content > hr::before {\n  display: none;\n}\nbody .callout[data-callout=statblock-pf2e] .callout-content > hr::after {\n  display: none;\n}\nbody .callout[data-callout=statblock-pf2e] .callout-content > hr:has(.admonition):has(.is-live-preview) {\n  margin-block-start: 0.5em;\n}\nbody .admonition-statblock-pf2e-parent .admonition-content > p {\n  margin-block-end: 0.25em;\n  margin-block-start: 0.5em;\n}\n\n.creature {\n  float: right;\n  margin-right: 0.5em;\n}\n\n.sourcebook {\n  float: right;\n  margin-bottom: 0.5em;\n  margin-right: 0.5em;\n}\n\nbody .callout[data-callout=statblock-pf2e] .callout[data-callout-metadata~=no-title] > .callout-title {\n  display: none;\n}\n\nbody .markdown-reading-view .callout[data-callout=statblock-pf2e] {\n  width: 70%;\n}\n\nbody .markdown-reading-view .callout[data-callout=statblock-pf2e]:has(.creature-statblock-container) {\n  width: 100%;\n}\n\nbody .is-live-preview .callout[data-callout=statblock-pf2e] {\n  width: 70%;\n}\n\nbody .is-live-preview .callout[data-callout=statblock-pf2e]:has(.creature-statblock-container) {\n  width: 100%;\n}\n\n.published-container .callout[data-callout=statblock-pf2e] {\n  max-width: 70%;\n  min-width: 40%;\n}\n\n.pathfinder.pathfinder.pathfinder h1 a.internal-link[href$=\"#Actions\"][title=\"Single Action\"] {\n  vertical-align: text-bottom;\n}\n.pathfinder.pathfinder.pathfinder h1 a.internal-link[href$=\"#Actions\"][title=Two-Action], .pathfinder.pathfinder.pathfinder a.internal-link[href$=\"#Actions\"][title=Two-Action] .callout-title-inner {\n  width: 2.5em;\n  vertical-align: text-bottom;\n}\n.pathfinder.pathfinder.pathfinder .admonition[data-callout=embed-ability] .admonition-content ol > li::marker, .pathfinder.pathfinder.pathfinder .admonition[data-callout=embed-ability] .admonition-content ul > li::marker {\n  color: transparent;\n}\n"
  },
  {
    "path": "examples/css-snippets/pf2-only-statblocks.css",
    "content": "/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-only-statblocks.scss */\n@import \"https://fonts.googleapis.com/css2?family=Oswald:wght@700&display=swap\";\nbody .callout[data-callout=statblock-pf2e] {\n  background-color: rgb(246, 244, 242);\n  border: none;\n  --callout-color: var(--statblock-pf2e);\n  color: rgb(0, 0, 0);\n  font-family: sans-serif;\n  font-size: 16px;\n  font-weight: 400;\n  line-height: 1.3em;\n  margin: 1rem 0;\n  min-width: 50%;\n  mix-blend-mode: normal;\n  padding: 0 0.25em;\n}\n.theme-dark body .callout[data-callout=statblock-pf2e] {\n  --statblock-pf2e: 201, 60, 60;\n}\n.theme-light body .callout[data-callout=statblock-pf2e] {\n  --statblock-pf2e: 201, 60, 60;\n}\nbody .callout[data-callout=statblock-pf2e] strong {\n  color: rgb(0, 0, 0);\n}\nbody .callout[data-callout=statblock-pf2e] em {\n  color: rgb(0, 0, 0);\n}\nbody .callout[data-callout=statblock-pf2e] strong + em {\n  color: rgb(0, 0, 0);\n}\nbody .callout[data-callout=statblock-pf2e] .callout-title {\n  background-color: rgb(246, 244, 242);\n  border: none;\n  border-radius: 0;\n  color: rgb(0, 0, 0);\n  display: flex;\n  font-family: \"Oswald\", sans-serif;\n  font-size: inherit;\n  gap: 0;\n  line-height: 1.3;\n  margin-bottom: 0;\n  padding: 0.25em 0 0;\n}\nbody .callout[data-callout=statblock-pf2e] .callout-title .callout-icon {\n  display: none;\n}\nbody .callout[data-callout=statblock-pf2e] .callout-title .callout-title-inner {\n  color: rgb(0, 0, 0);\n  flex: 1;\n  font-size: 1.35em;\n  font-weight: 700;\n  line-height: 1;\n  margin-bottom: 0;\n  margin-left: 0.25em;\n  padding-bottom: 0;\n  position: relative;\n  text-align: left;\n  text-transform: uppercase;\n}\nbody .callout[data-callout=statblock-pf2e] img[src$=\"#token\"],\nbody .callout[data-callout=statblock-pf2e] div[src$=\"#token\"] {\n  float: right;\n  margin-left: 0.3125em;\n  width: 9.375em;\n}\nbody .callout[data-callout=statblock-pf2e] .callout-content {\n  background-color: rgb(246, 244, 242);\n  margin-top: 0;\n  padding-left: 0.25em;\n  padding-right: 0.25em;\n  padding-top: 0;\n}\nbody .callout[data-callout=statblock-pf2e] .callout-content a {\n  color: rgb(51, 122, 183);\n  font-weight: 700;\n  text-decoration: none;\n}\nbody .callout[data-callout=statblock-pf2e] .callout-content a.external-link {\n  background-image: none;\n  background-size: 0;\n  color: rgb(150, 122, 222);\n  padding-right: 0;\n}\nbody .callout[data-callout=statblock-pf2e] .callout-content a.external-link::after {\n  display: none;\n}\nbody .callout[data-callout=statblock-pf2e] .callout-content .internal-link.is-unresolved::after {\n  display: none;\n}\nbody .callout[data-callout=statblock-pf2e] .callout-content blockquote {\n  background-color: rgb(246, 244, 242);\n  border: none;\n  color: rgb(0, 0, 0);\n  margin-inline-end: 1em;\n  margin-inline-start: 2em;\n  padding: 0;\n}\nbody .callout[data-callout=statblock-pf2e] .callout-content > p {\n  margin-block-start: 0.5em;\n}\nbody .callout[data-callout=statblock-pf2e] .callout-content li {\n  line-height: 1.2em;\n  margin-block-start: 0.5em;\n}\nbody .callout[data-callout=statblock-pf2e] .callout-content > hr {\n  border-color: rgb(0, 0, 0);\n  border-top: 1px solid;\n  height: 1px;\n  margin: 0;\n  width: 100%;\n}\nbody .callout[data-callout=statblock-pf2e] .callout-content > hr::before {\n  display: none;\n}\nbody .callout[data-callout=statblock-pf2e] .callout-content > hr::after {\n  display: none;\n}\nbody .callout[data-callout=statblock-pf2e] .callout-content > hr:has(.admonition):has(.is-live-preview) {\n  margin-block-start: 0.5em;\n}\nbody .admonition-statblock-pf2e-parent .admonition-content > p {\n  margin-block-end: 0.25em;\n  margin-block-start: 0.5em;\n}\n\n.creature {\n  float: right;\n  margin-right: 0.5em;\n}\n\n.sourcebook {\n  float: right;\n  margin-bottom: 0.5em;\n  margin-right: 0.5em;\n}\n\nbody .callout[data-callout=statblock-pf2e] .callout[data-callout-metadata~=no-title] > .callout-title {\n  display: none;\n}\n\nbody .markdown-reading-view .callout[data-callout=statblock-pf2e] {\n  width: 70%;\n}\n\nbody .markdown-reading-view .callout[data-callout=statblock-pf2e]:has(.creature-statblock-container) {\n  width: 100%;\n}\n\nbody .is-live-preview .callout[data-callout=statblock-pf2e] {\n  width: 70%;\n}\n\nbody .is-live-preview .callout[data-callout=statblock-pf2e]:has(.creature-statblock-container) {\n  width: 100%;\n}\n\n.published-container .callout[data-callout=statblock-pf2e] {\n  max-width: 70%;\n  min-width: 40%;\n}\n\nbody .callout[data-callout=statblock-pf2e][title=\"Common Rarity Trait\"] {\n  background: rgb(232, 232, 232);\n  border-color: rgb(232, 232, 232);\n  background-clip: border-box;\n  border-radius: 0.3125rem;\n  border-style: solid;\n  color: rgb(0, 0, 0) !important;\n  cursor: help;\n  font-weight: 700;\n  padding: 0.0625rem;\n  margin: 0.125rem;\n  text-align: center;\n  text-indent: 0;\n  word-break: keep-all;\n  font-style: normal;\n  display: inline-block;\n  border-width: 0.0625rem;\n}\nli body .callout[data-callout=statblock-pf2e][title=\"Common Rarity Trait\"] {\n  margin-bottom: 0;\n}\nbody .callout[data-callout=statblock-pf2e][title=\"Uncommon Rarity Trait\"] {\n  background: rgb(152, 81, 61);\n  border-color: rgb(152, 81, 61);\n  background-clip: border-box;\n  border-radius: 0.3125rem;\n  border-style: solid;\n  color: rgb(232, 232, 232) !important;\n  cursor: help;\n  font-weight: 700;\n  padding: 0.0625rem;\n  margin: 0.125rem 0.125rem 0.75rem;\n  text-align: center;\n  text-indent: 0;\n  word-break: keep-all;\n  font-style: normal;\n  display: inline-block;\n}\nli body .callout[data-callout=statblock-pf2e][title=\"Uncommon Rarity Trait\"] {\n  margin-bottom: 0;\n}\nbody .callout[data-callout=statblock-pf2e][title=\"Rare Rarity Trait\"] {\n  background: rgb(0, 38, 100);\n  border-color: rgb(0, 38, 100);\n  background-clip: border-box;\n  border-radius: 0.3125rem;\n  border-style: solid;\n  color: rgb(232, 232, 232) !important;\n  cursor: help;\n  font-weight: 700;\n  padding: 0.0625rem;\n  margin: 0.125rem 0.125rem 0.75rem;\n  text-align: center;\n  text-indent: 0;\n  word-break: keep-all;\n  font-style: normal;\n  display: inline-block;\n}\nli body .callout[data-callout=statblock-pf2e][title=\"Rare Rarity Trait\"] {\n  margin-bottom: 0;\n}\nbody .callout[data-callout=statblock-pf2e][title=\"Unique Rarity Trait\"] {\n  background: rgb(84, 22, 110);\n  border-color: rgb(84, 22, 110);\n  background-clip: border-box;\n  border-radius: 0.3125rem;\n  border-style: solid;\n  color: rgb(232, 232, 232) !important;\n  cursor: help;\n  font-weight: 700;\n  padding: 0.0625rem;\n  margin: 0.125rem 0.125rem 0.75rem;\n  text-align: center;\n  text-indent: 0;\n  word-break: keep-all;\n  font-style: normal;\n  display: inline-block;\n}\nli body .callout[data-callout=statblock-pf2e][title=\"Unique Rarity Trait\"] {\n  margin-bottom: 0;\n}\nbody .callout[data-callout=statblock-pf2e][title*=\"Alignment Trait\"] {\n  background: rgb(102, 111, 153);\n  border-color: rgb(102, 111, 153);\n  background-clip: border-box;\n  border-radius: 0.3125rem;\n  border-style: solid;\n  color: rgb(232, 232, 232) !important;\n  cursor: help;\n  font-weight: 700;\n  padding: 0.0625rem;\n  margin: 0.125rem 0.125rem 0.75rem;\n  text-align: center;\n  text-indent: 0;\n  word-break: keep-all;\n  font-style: normal;\n  display: inline-block;\n}\nli body .callout[data-callout=statblock-pf2e][title*=\"Alignment Trait\"] {\n  margin-bottom: 0;\n}\nbody .callout[data-callout=statblock-pf2e][title*=\"Size Trait\"] {\n  background: rgb(82, 122, 95);\n  border-color: rgb(82, 122, 95);\n  background-clip: border-box;\n  border-radius: 0.3125rem;\n  border-style: solid;\n  color: rgb(232, 232, 232) !important;\n  cursor: help;\n  font-weight: 700;\n  padding: 0.0625rem;\n  margin: 0.125rem 0.125rem 0.75rem;\n  text-align: center;\n  text-indent: 0;\n  word-break: keep-all;\n  font-style: normal;\n  display: inline-block;\n}\nli body .callout[data-callout=statblock-pf2e][title*=\"Size Trait\"] {\n  margin-bottom: 0;\n}\nbody .callout[data-callout=statblock-pf2e][title*=\"Action & Ability Trait\"], body .callout[data-callout=statblock-pf2e][title*=\"Ancestry & Heritage Trait\"], body .callout[data-callout=statblock-pf2e][title*=\"Armor Trait\"], body .callout[data-callout=statblock-pf2e][title*=\"Class Trait\"], body .callout[data-callout=statblock-pf2e][title*=\"Combat Trait\"], body .callout[data-callout=statblock-pf2e][title*=\"Creature Trait\"], body .callout[data-callout=statblock-pf2e][title*=\"Creature Type Trait\"], body .callout[data-callout=statblock-pf2e][title*=\"Effect Trait\"], body .callout[data-callout=statblock-pf2e][title*=\"Element Trait\"], body .callout[data-callout=statblock-pf2e][title*=\"Equipment Trait\"], body .callout[data-callout=statblock-pf2e][title*=\"Feat Trait\"], body .callout[data-callout=statblock-pf2e][title*=\"General Trait\"], body .callout[data-callout=statblock-pf2e][title*=\"Gravity Trait\"], body .callout[data-callout=statblock-pf2e][title*=\"Hazard Trait\"], body .callout[data-callout=statblock-pf2e][title*=\"Item Trait\"], body .callout[data-callout=statblock-pf2e][title*=\"Morphic Trait\"], body .callout[data-callout=statblock-pf2e][title*=\"Planar Trait\"], body .callout[data-callout=statblock-pf2e][title*=\"Settlement Trait\"], body .callout[data-callout=statblock-pf2e][title*=\"School Trait\"], body .callout[data-callout=statblock-pf2e][title*=\"Spell Trait\"], body .callout[data-callout=statblock-pf2e][title*=\"Tradition Trait\"], body .callout[data-callout=statblock-pf2e][title*=\"Weapon Trait\"] {\n  background: rgb(97, 20, 5);\n  border-color: rgb(97, 20, 5);\n  background-clip: border-box;\n  border-radius: 0.3125rem;\n  border-style: solid;\n  color: rgb(232, 232, 232) !important;\n  cursor: help;\n  font-weight: 700;\n  padding: 0.0625rem;\n  margin: 0.125rem 0.125rem 0.75rem;\n  text-align: center;\n  text-indent: 0;\n  word-break: keep-all;\n  font-style: normal;\n  display: inline-block;\n}\nli body .callout[data-callout=statblock-pf2e][title*=\"Action & Ability Trait\"], li body .callout[data-callout=statblock-pf2e][title*=\"Ancestry & Heritage Trait\"], li body .callout[data-callout=statblock-pf2e][title*=\"Armor Trait\"], li body .callout[data-callout=statblock-pf2e][title*=\"Class Trait\"], li body .callout[data-callout=statblock-pf2e][title*=\"Combat Trait\"], li body .callout[data-callout=statblock-pf2e][title*=\"Creature Trait\"], li body .callout[data-callout=statblock-pf2e][title*=\"Creature Type Trait\"], li body .callout[data-callout=statblock-pf2e][title*=\"Effect Trait\"], li body .callout[data-callout=statblock-pf2e][title*=\"Element Trait\"], li body .callout[data-callout=statblock-pf2e][title*=\"Equipment Trait\"], li body .callout[data-callout=statblock-pf2e][title*=\"Feat Trait\"], li body .callout[data-callout=statblock-pf2e][title*=\"General Trait\"], li body .callout[data-callout=statblock-pf2e][title*=\"Gravity Trait\"], li body .callout[data-callout=statblock-pf2e][title*=\"Hazard Trait\"], li body .callout[data-callout=statblock-pf2e][title*=\"Item Trait\"], li body .callout[data-callout=statblock-pf2e][title*=\"Morphic Trait\"], li body .callout[data-callout=statblock-pf2e][title*=\"Planar Trait\"], li body .callout[data-callout=statblock-pf2e][title*=\"Settlement Trait\"], li body .callout[data-callout=statblock-pf2e][title*=\"School Trait\"], li body .callout[data-callout=statblock-pf2e][title*=\"Spell Trait\"], li body .callout[data-callout=statblock-pf2e][title*=\"Tradition Trait\"], li body .callout[data-callout=statblock-pf2e][title*=\"Weapon Trait\"] {\n  margin-bottom: 0;\n}\nbody .callout[data-callout=statblock-pf2e] a.internal-link[href$=\"#Actions\"][title=\"Single Action\"] {\n  background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 2040 1024'%3E%3Cpath d='M1022 512L510 0 263 247l265 265-265 265 247 247 512-512zM185 329L2 512l183 183 183-183-183-183z'/%3E%3C/svg%3E\");\n  background-size: cover;\n  background-repeat: no-repeat;\n  display: inline-block;\n  vertical-align: top;\n  content: \"\";\n  width: 1em;\n  height: 1em;\n  color: transparent;\n}\nbody .callout[data-callout=statblock-pf2e] a.internal-link[href$=\"#Actions\"][title=Two-Action] {\n  background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 2040 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1018 512L507 1 261 248l264 264-264 264 246 247 511-511zM183 329L0 512l183 183 183-183-183-183zm1417 175L1152 56 935 272l232 232-232 232 217 217 448-449z'/%3E%3C/svg%3E\");\n  background-repeat: no-repeat;\n  background-size: 1.5625rem;\n  line-height: 1.5625rem;\n  vertical-align: top;\n  content: \"\";\n  width: 1em;\n  height: 1em;\n  color: transparent;\n}\nbody .callout[data-callout=statblock-pf2e] a.internal-link[href$=\"#Actions\"][title=Three-Action] {\n  background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 2128 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m1025 512-512-512-247 247 265 265-265 265 247 247zm1098-10-360-360-175 174 187 186-187 187 175 174zm-1935-173-183 183 183 183 183-183zm1427 175-449-449-218 217 233 232-233 233 218 217z'/%3E%3C/svg%3E\");\n  background-repeat: no-repeat;\n  line-height: 1.5625rem;\n  vertical-align: top;\n  content: \"\";\n  width: 1em;\n  height: 1em;\n  color: transparent;\n}\nbody .callout[data-callout=statblock-pf2e] a.internal-link[href$=\"#Actions\"][title=Reaction] {\n  background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 2128 1024'%3E%3Cpath d='M1013 377q-23-71-82-128-60-57-143.5-94T603 105q-101-14-207 1-75 11-140 35t-116 58q-51 34-87 77-36 42-53 89 21-26 50-48 29-23 64.5-41t76.5-31q42-13 88-20 85-12 166-1t148.5 40.5Q661 294 709 340t66 103q18 58 2 113-17 55-60.5 100.5T605 735q-67 32-152 44-28 4-55.5 5.5t-54.5.5q63 14 131.5 17t137.5-7q106-15 190-56 84-40 138-96.5t75-125.5q20-68-2-140zM152 823l489 103-109-149 56-215-436 261z'/%3E%3C/svg%3E\");\n  background-size: cover;\n  display: inline-block;\n  vertical-align: middle;\n  content: \"\";\n  width: 1em;\n  height: 1em;\n  color: transparent;\n}\nbody .callout[data-callout=statblock-pf2e] a.internal-link[href$=\"#Actions\"][title=\"Free Action\"] {\n  background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 2128 1024'%3E%3Cpath d='M516 0L267 250 0 518l509 506 515-515L516 0zM188 496l109-109 109 109-109 109-109-109zm311 380L386 763l251-251-245-246 124-124 359 359-376 375z'/%3E%3C/svg%3E\");\n  background-size: cover;\n  display: inline-block;\n  vertical-align: top;\n  content: \"\";\n  width: 1em;\n  height: 1em;\n  color: transparent;\n}\nbody .callout[data-callout=statblock-pf2e] a.internal-link[href$=\"#Actions\"][title=Varies] {\n  background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpath d='M16 2c-7.732 0-14 6.268-14 14s6.268 14 14 14 14-6.268 14-14-6.268-14-14-14zM28.998 15.952c-3.808 1.889-8.19 1.875-11.971 0 3.625-2.404 5.958-6.273 6.398-10.608 3.352 2.343 5.557 6.22 5.573 10.608zM22.46 4.735c-0.273 4.234-2.484 8.022-5.989 10.353-0.268-4.347-2.447-8.296-5.983-10.845 1.677-0.789 3.54-1.243 5.512-1.243 2.352 0 4.555 0.638 6.46 1.735zM9.471 4.776c3.532 2.354 5.708 6.151 5.974 10.359-3.9-1.941-8.413-2.028-12.389-0.238 0.365-4.322 2.853-8.040 6.415-10.121zM3.002 16.047c3.808-1.887 8.192-1.873 11.972 0.001-3.628 2.405-5.96 6.27-6.4 10.607-3.352-2.343-5.556-6.22-5.572-10.608zM9.542 27.266c0.273-4.236 2.478-8.021 5.987-10.354 0.268 4.352 2.447 8.295 5.984 10.844-1.676 0.79-3.54 1.244-5.513 1.244-2.352 0-4.553-0.637-6.458-1.734zM22.531 27.222c-3.534-2.354-5.711-6.145-5.976-10.359 2.035 1.013 4.237 1.523 6.442 1.523 2.025 0 4.045-0.455 5.949-1.314-0.357 4.334-2.845 8.064-6.415 10.15z'%3E%3C/path%3E%3C/svg%3E\");\n  background-size: cover;\n  display: inline-block;\n  vertical-align: top;\n  content: \"\";\n  width: 1em;\n  height: 1em;\n  color: transparent;\n}\nbody .callout[data-callout=statblock-pf2e] a.internal-link[href$=\"#Actions\"][title=\"Duration or Frequency\"] {\n  background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' version='1.1' width='20' height='20' viewBox='0 0 20 20'%3E%3Ctitle%3Ehour-glass%3C/title%3E%3Cpath d='M15.6 4.576c0-2.139 0-2.348 0-2.348 0-0.789-2.508-2.228-5.6-2.228-3.093 0-5.6 1.439-5.6 2.228 0 0 0 0.209 0 2.348 0 2.141 3.877 3.908 3.877 5.424 0 1.514-3.877 3.281-3.877 5.422s0 2.35 0 2.35c0 0.788 2.507 2.228 5.6 2.228 3.092 0 5.6-1.44 5.6-2.229 0 0 0-0.209 0-2.35s-3.877-3.908-3.877-5.422c0-1.515 3.877-3.282 3.877-5.423zM5.941 2.328c0.696-0.439 2-1.082 4.114-1.082s4.006 1.082 4.006 1.082c0.142 0.086 0.698 0.383 0.317 0.609-0.838 0.497-2.478 1.020-4.378 1.020s-3.484-0.576-4.324-1.074c-0.381-0.225 0.265-0.555 0.265-0.555zM10.501 10c0 1.193 0.996 1.961 2.051 2.986 0.771 0.748 1.826 1.773 1.826 2.435v1.328c-0.97-0.483-3.872-0.955-3.872-2.504 0-0.783-1.013-0.783-1.013 0 0 1.549-2.902 2.021-3.872 2.504v-1.328c0-0.662 1.056-1.688 1.826-2.435 1.055-1.025 2.051-1.793 2.051-2.986s-0.996-1.961-2.051-2.986c-0.771-0.75-1.826-1.775-1.826-2.438l-0.046-0.998c1.026 0.553 2.652 1.078 4.425 1.078 1.772 0 3.406-0.525 4.433-1.078l-0.055 0.998c0 0.662-1.056 1.688-1.826 2.438-1.054 1.025-2.051 1.793-2.051 2.986z'%3E%3C/path%3E%3C/svg%3E%0A\");\n  background-size: cover;\n  display: inline-block;\n  vertical-align: top;\n  content: \"\";\n  width: 1em;\n  height: 1em;\n  color: transparent;\n}\n"
  },
  {
    "path": "examples/templates/README.md",
    "content": "# Templates\n\nThis application uses the [Qute Templating Engine](https://quarkus.io/guides/qute). You can make simple customizations to markdown output by copying a template from `src/main/resources/templates`, making the desired modifications, and specifying that template in your configuration file under the `template` key:\n\n``` json\n{\n  \"from\": [\n    \"DMG\",\n    \"PHB\",\n    \"MM\"\n  ],\n  \"paths\": {\n    \"compendium\": \"z_compendium/\",\n    \"rules\": \"z_compendium/rules\"\n  },\n  \"template\": {\n    \"background\": \"examples/templates/tools5e/images-background2md.txt\",\n    \"monster\": \"examples/templates/tools5e/monster2md-scores.txt\"\n  }\n}\n```\n\nThe flag used to specify a template corresponds to the type of template being used.\n\n- Valid template keys for 5etools: `background`, `class`, `deck`, `deity`, `feat`, `hazard`, `index.txt`, `item`, `monster`, `note`, `object`, `psionic`, `race`, `reward`, `spell`, `subclass`, `vehicle`.\n- Valid template keys for Pf2eTools: `ability`, `action`, `affliction`, `archetype`, `background`, `book`, `deity`, `feat`, `hazard`, `inline-ability`, `inline-affliction`, `inline-attack`, `item`, `note`, `ritual`, `spell`, `trait`.\n\n## Customizing templates\n\nNot everything is customizable. In some cases, indenting, organizing, formatting, and linking text accurately is easier to do inline as a big blob.\n\n- See the **[Template Reference](../../docs/templates/)** for information about attributes you can use in your templates\n\n### Examples\n\n- [**5eTools Example Templates**](tools5e/).\n"
  },
  {
    "path": "examples/templates/tools5e/README.md",
    "content": "# 5eTools Templates for Obsidian\n\nThis guide explains how to use 5eTools data with Obsidian plugins like Fantasy Statblocks and Initiative Tracker.\n\n- [References](#references)\n- [Fantasy Statblocks Integration](#fantasy-statblocks-integration)\n    - [Minimal YAML for Bestiary and Initive Tracker](#minimal-yaml-for-bestiary-and-initive-tracker)\n    - [Full statblock rendering](#full-statblock-rendering)\n- [Template Types](#template-types)\n    - [Default Templates](#default-templates)\n    - [5eTools templates with images](#5etools-templates-with-images)\n- [Display Variations](#display-variations)\n    - [Alternate Ability Score Display](#alternate-ability-score-display)\n    - [2024 Score Display (Table Format)](#2024-score-display-table-format)\n- [Advanced Techniques](#advanced-techniques)\n    - [Splitting Strings in Frontmatter](#splitting-strings-in-frontmatter)\n\n## References\n\n- [5eTools template reference](../../../docs/templates/dnd5e/README.md)\n- [Monster template reference](../../../docs/templates/dnd5e/QuteMonster/README.md)\n\n## Fantasy Statblocks Integration\n\nThethe *Fantasy Statblocks* plugin provides a bestiary that can be used with *Initiative Tracker* to create encounters.\n\n**Monsters**, **Objects**, and **Vehicles** provide attributes that can be used with the *Fantasy Statblocks* bestiary.\n\n- [Minimal YAML for Bestiary and Initive Tracker](#minimal-yaml-for-bestiary-and-initive-tracker)\n- [Full statblock rendering](#full-statblock-rendering)\n\n### Minimal YAML for Bestiary and Initive Tracker\n\nUse this approach to make creatures available to the Initiative Tracker for encounters.\n\n1. Add to your note's frontmatter:\n\n    ```yaml\n    statblock: true\n    statblock-link: \"#^statblock\"\n    {resource.5eInitiativeYaml}\n    ```\n\n    - `statblock: inline` tells *Fantasy Statblocks* that the note defines a creature.\n    - `statblock-link` specifies content that should be linked to / embeded in the *Initiative Tracker* and *Combatant/Creature* views\n    - `{resource.5eInitiativeYaml}` or `{resource.5eInitiativeYamlNoSource}` will add only the attributes *Initiative Tracker* needs\n\n2. In your note body, create a block reference (`^statblock) after the content you want displayed in the creature view:\n\n    ````md\n    ```ad-statblock\n\n    ...statblock content...\n\n    ```\n    ^statblock\n    ````\n\nExamples:\n\n- [monster2md-yamlStatblock-header.txt](monster2md-yamlStatblock-header.txt)\n- [object2md-yamlStatblock-header.txt](object2md-yamlStatblock-header.txt)\n\n### Full statblock rendering\n\nThis approach will use *Fantasy Statblocks* rendering.\n\n1. Add `statblock: inline` to the note's frontmatter to tell *Fantasy Statblocks* that the note defines a creature.\n\n    ```yaml\n    statblock: true\n    ```\n\n2. In your note body, use:\n\n    ````markdown\n    ```statblock\n    {resource.5eStatblockYaml}\n    ```\n\n    Or, to hide the source suffix:\n\n    ````markdown\n    ```statblock\n    {resource.5eStatblockYamlNoSource}\n    ```\n    ````\n\nExamples:\n\n- [monster2md-yamlStatblock-body.txt](monster2md-yamlStatblock-body.txt)\n- [object2md-yamlStatblock-body.txt](object2md-yamlStatblock-body.txt)\n\n> [!TIP]\n> If you're using the *Fantasy Statblocks* plugin to render statblocks\n> and you use the Dice Roller plugin, you'll want to set the following\n> CLI config values:\n>\n> ```json\n> \"useDiceRoller\" : true,\n> \"yamlStatblocks\" : true,\n> ```\n\n## Template Types\n\n### Default Templates\n\nAvailable template types include:\n\n- monster\n- spell\n- item\n- background\n- class\n- object\n- vehicle\n- race\n- and more\n\nFull list: [5eTools default templates](../../../src/main/resources/templates/tools5e/)\n\n### 5eTools templates with images\n\nSome 5eTools data types have fluff images.  These templates include those images in the markdown.\n\n- [images-background2md.txt](images-background2md.txt)\n- [images-item2md.txt](images-item2md.txt)\n- [images-monster2md.txt](images-monster2md.txt)\n- [images-object2md.txt](images-object2md.txt)\n- [images-race2md.txt](images-race2md.txt)\n- [images-spell2md.txt](images-spell2md.txt)\n- [images-vehicle2md.txt](images-vehicle2md.txt)\n\nFull list: any template beginning with \"images\" [5eTools example templates](./)\n\n## Display Variations\n\n### Alternate Ability Score Display\n\n````markdown\n```ad-statblock\n...\n- STR: {resource.scores.str} `dice: 1d20 {resource.scores.strMod}`\n- DEX: {resource.scores.dex} `dice: 1d20 {resource.scores.dexMod}`\n- CON: {resource.scores.con} `dice: 1d20 {resource.scores.conMod}`\n- INT: {resource.scores.int} `dice: 1d20 {resource.scores.intMod}`\n- WIS: {resource.scores.wis} `dice: 1d20 {resource.scores.wisMod}`\n- CHA: {resource.scores.cha} `dice: 1d20 {resource.scores.chaMod}`\n...\n```\n^statblock\n````\n\nExample:\n\n- [monster2md-scores.txt](monster2md-scores.txt) (similar will work for objects)\n\n### 2024 Score Display (Table Format)\n\nThis pulls things apart a little differently.\n\n- `resource.scores.*Stat` will show the raw score\n- `resource.scores.*Mod` will show the modifier\n- `resource.savesSkills.saveOrDefault.*` will render the saving throw (in bold) or the default modifier\n\n```md\n|   | STAT  |  MOD | SAVE |\n|:--|:-:|:----:|:----:|\n|Str| {resource.scores.strStat} | {resource.scores.strMod} | {resource.savesSkills.saveOrDefault.strength} |\n|Dex| {resource.scores.dexStat} | {resource.scores.dexMod} | {resource.savesSkills.saveOrDefault.dexterity} |\n|Con| {resource.scores.conStat} | {resource.scores.conMod} | {resource.savesSkills.saveOrDefault.constitution} |\n|Int| {resource.scores.intStat} | {resource.scores.intMod} | {resource.savesSkills.saveOrDefault.intelligence} |\n|Wis| {resource.scores.wisStat} | {resource.scores.wisMod} | {resource.savesSkills.saveOrDefault.wisdom} |\n|Cha| {resource.scores.chaStat} | {resource.scores.chaMod} | {resource.savesSkills.saveOrDefault.charisma} |\n```\n\nRaw types are [here](../../../docs/templates/dnd5e/QuteMonster/SavesAndSkills.md)\n\n## Advanced Techniques\n\n### Splitting Strings in Frontmatter\n\n```md\n{#if resource.conditionImmune}\nconditionImmunities:\n{#for condition in resource.conditionImmune.split(\", ?\")}\n- \"{condition}\"\n{/for}\n{/if}\n```\n\n### Display as JSON\n\nTo help debug what attributes are available, you can have it rendered as a JSON string.\n\nTwo examples:\n\n```md\n{resource.conditionImmune.jsonString}\n\n{resource.savesSkills.jsonString}\n```\n"
  },
  {
    "path": "examples/templates/tools5e/images-background2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-background\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n*Source: {resource.source}*  \n{#if resource.hasImages }{resource.showPortraitImage}{/if}\n\n{#if resource.prerequisite}\n***Prerequisites*** {resource.prerequisite}\n\n{/if}\n{resource.text}\n{#if resource.hasMoreImages }\n\n{resource.showMoreImages}\n{/if}\n"
  },
  {
    "path": "examples/templates/tools5e/images-class2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-class\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n*Source: {resource.source}*  \n\n{#if resource.classProgression }\n{resource.classProgression}\n\n{/if}\n{#if resource.hasImages }{resource.showPortraitImage}\n\n{/if}\n## Hit Points\n\n{#if resource.hitDice }\n- **Hit Dice**: 1d{resource.hitDice} per {resource.name} level\n- **Hit Points at First Level:** {resource.hitDice} + CON\n- **Hit Points at Higher Levels:** add {resource.hitRollAverage} OR 1d{resource.hitDice} + CON  (minimum of 1)\n{#else}\n- **Hit Dice**: *x* = hit dice specified in the sidekick's statblock (human, gnome, kobold, etc.)\n- **Hit Points at First Level:** *x* + CON\n- **Hit Points at Higher Levels:** add 1d*x* + CON (minimum of 1)\n{/if}\n\n## Starting {resource.name}\n\n{resource.startingEquipment}\n\n{#if resource.multiclassing }\n## Multiclassing {resource.name}\n\n{resource.multiclassing}\n{/if}{#if resource.hasMoreImages }\n\n{resource.showMoreImages}\n\n{/if}\n{resource.text}\n"
  },
  {
    "path": "examples/templates/tools5e/images-item2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-item\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}\n{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n{#if resource.detail }*{resource.detail}*  \n{/if}{#if resource.hasImages }{resource.showPortraitImage}{/if}\n\n{#if resource.prerequisite}\n- **Prerequisites**: {resource.prerequisite}\n{/if}{#if resource.armorClass }\n- **Armor Class**: {resource.armorClass}\n{#else if resource.damage }{#if resource.damage2h }\n- **Damage**:\n  - One-handed: {resource.damage}\n  - Two-handed: {resource.damage2h}\n{#else}\n- **Damage**: {resource.damage}\n{/if}{#if resource.range }\n- **Range**: {resource.range}\n{/if}{/if}{#if resource.properties }\n- **Properties**: {resource.properties}\n{/if}{#if resource.strengthRequirement }\n- **Strength**: Requires {resource.strengthRequirement} STR.\n{/if}{#if resource.stealthPenalty }\n- **Stealth**: The wearer has disadvantage on Stealth (DEX) checks.\n{/if}{#if resource.cost }\n- **Cost**: {resource.cost}\n{/if}{#if resource.weight }\n- **Weight**: {resource.weight} lbs.\n{/if}{#if resource.text }\n\n{resource.text}\n{/if}{#if resource.hasMoreImages }\n\n{resource.showMoreImages}\n\n{/if}{#if resource.variants }\n\n**Variants**:\n{resource.variantSectionLinks}\n\n{#for variant in resource.variants}\n### {variant.name}\n\n{#if variant.prerequisite}\n- **Prerequisites**: {variant.prerequisite}\n{/if}{#if variant.armorClass }\n- **Armor Class**: {variant.armorClass}\n{#else if variant.damage }{#if variant.damage2h }\n- **Damage**:\n  - One-handed: {variant.damage}\n  - Two-handed: {variant.damage2h}\n{#else}\n- **Damage**: {variant.damage}\n{/if}{#if variant.range }\n- **Range**: {variant.range}\n{/if}{/if}{#if variant.properties }\n- **Properties**: {variant.properties}\n{/if}{#if variant.strengthRequirement }\n- **Strength**: Requires {variant.strengthRequirement} STR.\n{/if}{#if variant.stealthPenalty }\n- **Stealth**: The wearer has disadvantage on Stealth (DEX) checks.\n{/if}{#if variant.cost }\n- **Cost**: {variant.cost}\n{/if}{#if variant.weight }\n- **Weight**: {variant.weight} lbs.\n{/if}\n\n{/for}\n{/if}\n\n*Source: {resource.source}*\n"
  },
  {
    "path": "examples/templates/tools5e/images-monster2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-monster\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n*Source: {resource.source}*  \n{#if resource.description }\n{#if resource.hasImages }{resource.showPortraitImage}{/if}\n\n{resource.description}\n{#if resource.hasMoreImages }\n\n{resource.showMoreImages}\n{/if}\n{#else if resource.hasImages}\n{resource.showAllImages}\n{/if}{#if resource.hasSections }\n\n## Statblock\n{/if}\n\n```ad-statblock\ntitle: {resource.name}{#if resource.token}\n![{resource.token.title}]({resource.token.vaultPath}#token){/if}\n*{resource.size} {resource.fullType}, {resource.alignment}*\n\n- **Armor Class** {#if resource.ac }{resource.ac} {/if}{#if resource.acText }({resource.acText}){/if}\n- **Hit Points** {resource.hp} {#if resource.hitDice }(`{resource.hitDice}`){/if} {#if resource.hpText }({resource.hpText}){/if}\n- **Speed** {resource.speed}\n\n|STR|DEX|CON|INT|WIS|CHA|\n|:---:|:---:|:---:|:---:|:---:|:---:|\n|{resource.scores}|\n\n- **Proficiency Bonus** {resource.pb}\n- **Saving Throws** {#if resource.savingThrows }{resource.savingThrows}{#else}⏤{/if}\n- **Skills** {#if resource.skills }{resource.skills}{#else}⏤{/if}\n- **Senses** {#if resource.senses }{resource.senses}, {/if}passive Perception {resource.passive}\n{#if resource.vulnerable }\n- **Damage Vulnerabilities** {resource.vulnerable}\n{/if}{#if resource.resist}\n- **Damage Resistances** {resource.resist}\n{/if}{#if resource.immune}\n- **Damage Immunities** {resource.immune}\n{/if}{#if resource.conditionImmune}\n- **Condition Immunities** {resource.conditionImmune}\n{/if}{#if resource.gear}\n- **Gear** {resource.gear.join(\", \")}\n{/if}\n- **Languages** {#if resource.languages }{resource.languages}{#else}—{/if}\n- **Challenge** {resource.cr}\n{#if resource.trait}\n\n## Traits\n{#for trait in resource.trait}\n\n{#if trait.name }***{trait.name}.*** {/if}{trait.desc}\n{/for}{/if}{#if resource.action}\n\n## Actions\n{#for action in resource.action}\n\n{#if action.name }***{action.name}.*** {/if}{action.desc}\n{/for}{/if}{#if resource.bonusAction}\n\n## Bonus Actions\n{#for bonusAction in resource.bonusAction}\n\n{#if bonusAction.name }***{bonusAction.name}.*** {/if}{bonusAction.desc}\n{/for}{/if}{#if resource.reaction}\n\n## Reactions\n{#for reaction in resource.reaction}\n\n{#if reaction.name }***{reaction.name}.*** {/if}{reaction.desc}\n{/for}{/if}{#if resource.legendary}\n\n## Legendary Actions\n{#for legendary in resource.legendary}\n\n{#if legendary.name }***{legendary.name}.*** {/if}{legendary.desc}\n{/for}{/if}{#if resource.legendaryGroup}{#for group in resource.legendaryGroup}\n\n## {group.name}\n\n{group.desc}\n{/for}{/if}\n```\n^statblock\n{#if resource.environment }\n\n## Environment\n\n{resource.environment}\n{/if}\n"
  },
  {
    "path": "examples/templates/tools5e/images-object2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-object\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n*Source: {resource.source}* \n\n{#if resource.text }\n{#if resource.hasImages }{resource.showPortraitImage}\n\n{/if}\n{resource.text}\n{#if resource.hasMoreImages }\n\n{resource.showMoreImages}\n{/if}\n{#else if resource.hasImages }\n{resource.showAllImages}\n\n{/if}{#if resource.hasSections }\n## Statblock\n\n{/if}\n```ad-statblock\ntitle: {resource.name}{#if resource.token}\n![{resource.token.title}]({resource.token.vaultPath}#token){/if}\n*{resource.size} {resource.objectType}{#if resource.creatureType } ({resource.creatureType}){/if}*\n\n- **Armor Class** {#if resource.ac }{resource.ac} {/if}{#if resource.acText }({resource.acText}){/if}\n- **Hit Points** {resource.hp or ' '} {#if resource.hpText }({resource.hpText}){/if}\n- **Speed** {resource.speed}\n\n|STR|DEX|CON|INT|WIS|CHA|\n|:---:|:---:|:---:|:---:|:---:|:---:|\n|{resource.scores}|\n\n{#if resource.senses }\n- **Senses** {resource.senses}\n{/if}{#if resource.vulnerable }\n- **Damage Vulnerabilities** {resource.vulnerable}\n{/if}{#if resource.resist}\n- **Damage Resistances** {resource.resist}\n{/if}{#if resource.immune}\n- **Damage Immunities** {resource.immune}\n{/if}{#if resource.conditionImmune}\n- **Condition Immunities** {resource.conditionImmune}\n{/if}\n{#if resource.action}\n\n## Actions\n{#for action in resource.action}\n\n{#if action.name }***{action.name}.*** {/if}{action.desc}\n{/for}{/if}\n```\n^statblock\n\n"
  },
  {
    "path": "examples/templates/tools5e/images-race2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-race\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n*Source: {resource.source}*  \n{#if resource.hasImages }{resource.showPortraitImage}{/if}\n\n- **Ability Scores**: {resource.ability}\n{#if resource.type}\n- **Type**: {resource.type}\n{/if}\n- **Size**: {resource.size}\n- **Speed**: {resource.speed}\n{#if resource.spellcasting}\n- **Spellcasting**: {resource.spellcasting}\n{/if}\n\n## Traits\n\n{resource.traits}\n{#if resource.description}\n\n## Description\n\n{resource.description}\n\n{/if}\n{#if resource.hasMoreImages }\n\n{resource.showMoreImages}\n{/if}\n"
  },
  {
    "path": "examples/templates/tools5e/images-spell2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-spell\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}\n{/if}\n{#if resource.classes }\nclasses:\n{#each resource.classList}\n- {it}\n{/each}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n%%-- Embedded content starts on the next line. --%%\n*{resource.level}, {resource.school}{#if resource.ritual} (ritual){/if}*  \n{#if resource.hasImages }{resource.showPortraitImage}{/if}\n\n- **Casting time:** {resource.time}{#if resource.ritual} unless cast as a ritual{/if}\n- **Range:** {resource.range}\n- **Components:** {resource.components}\n- **Duration:** {resource.duration}\n\n{resource.text}\n\n{#if resource.hasSections }\n## Summary\n\n{/if}{#if resource.hasMoreImages }\n{resource.showMoreImages}\n\n{/if}{#if resource.classes }\n**Classes**: {resource.classes}\n\n{/if}\n*Source: {resource.source}*\n"
  },
  {
    "path": "examples/templates/tools5e/images-subclass2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-class\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}\n{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n*{resource.parentClassLink}{#if resource.subclassTitle}: {resource.subclassTitle}{/if}*  \n*Source: {resource.source}*  \n\n{#if resource.classProgression }\n{resource.classProgression}\n\n{/if}\n{#if resource.text }\n{#if resource.hasImages }{resource.showPortraitImage}\n\n{/if}\n{resource.text}\n{#if resource.hasMoreImages }\n\n{resource.showMoreImages}\n{/if}\n{#else if resource.hasImages}\n{resource.showAllImages}\n{/if}\n"
  },
  {
    "path": "examples/templates/tools5e/images-vehicle2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-vehicle\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n%%-- Embedded content starts on the next line. --%%\n*Source: {resource.source}*  \n{#if resource.text && !resource.isObject }{! ----- Fluff for non-OBJECT vehicles ----- !}\n{#if resource.hasImages }{resource.showPortraitImage}{/if}\n\n{resource.text}\n{#if resource.hasMoreImages }\n\n{resource.showMoreImages}\n\n{/if}\n{#else}{! ----- Fluff images for OBJECT vehicles (or no text) ----- !}\n{#if resource.hasImages }\n\n{resource.showAllImages}\n\n{/if}\n\n{/if}{#if !resource.isObject && resource.hasSections }\n## Statblock\n\n{/if}\n```ad-statblock\ntitle: {resource.name}{#if resource.token}\n![{resource.token.title}]({resource.token.vaultPath}#token){/if}\n*{resource.sizeDimension}; {resource.terrain}*\n{#if resource.shipCrewCargoPace}\n\n{resource.shipCrewCargoPace}\n{/if}{#if resource.isObject }{! ----- BEGIN OBJECT (type) ----- !}\n{#if resource.scores}\n\n|STR|DEX|CON|INT|WIS|CHA|\n|:---:|:---:|:---:|:---:|:---:|:---:|\n|{resource.scores}|\n\n{/if}{#if resource.immuneResist }\n{resource.immuneResist}\n{/if}{#if resource.text }\n\n{resource.text}\n\n{/if}{#if resource.shipSections}{#each resource.shipSections}\n\n{it}\n{/each}{/if}\n{#else if resource.isCreature }{! ----- BEGIN CREATURE (type) ----- !}\n{#if resource.scores}\n\n|STR|DEX|CON|INT|WIS|CHA|\n|:---:|:---:|:---:|:---:|:---:|:---:|\n|{resource.scores}|\n\n{/if}{#if resource.immuneResist }\n{resource.immuneResist}\n{/if}{#if resource.shipSections}{#each resource.shipSections}\n\n{it}\n{/each}{/if}\n{#else if resource.isWarMachine }{! ----- BEGIN INFERNAL WAR MACHINE (type) ----- !}\n{#if resource.scores}\n\n|STR|DEX|CON|INT|WIS|CHA|\n|:---:|:---:|:---:|:---:|:---:|:---:|\n|{resource.scores}|\n\n{/if}{#if resource.immuneResist }\n{resource.immuneResist}\n{/if}{#if resource.shipSections}{#each resource.shipSections}\n\n{it}\n{/each}{/if}\n{#else if resource.isSpelljammer }{! ----- BEGIN SPELLJAMMER (type) ----- !}\n{#if resource.shipSections}{#each resource.shipSections}\n\n{it}\n{/each}{/if}\n{#else if resource.isShip }{! ----- BEGIN SHIP (type) ----- !}\n{#if resource.scores}\n\n|STR|DEX|CON|INT|WIS|CHA|\n|:---:|:---:|:---:|:---:|:---:|:---:|\n|{resource.scores}|\n\n{/if}{#if resource.immuneResist }\n{resource.immuneResist}\n{/if}{#if resource.action}\n\n## Actions\n{#each resource.action}\n\n{it}\n{/each}{/if}{#if resource.shipSections}{#each resource.shipSections}\n\n{it}\n{/each}{/if}\n{/if}{! END SHIP (type) !}\n```\n^statblock\n"
  },
  {
    "path": "examples/templates/tools5e/mixed/README.md",
    "content": "---\ntype: fileIndex\n---\n# Additional (mixed) templates\n\nAdditional examples that mash together optional elements from other examples.\n\n- [`background`](mixed-background2md.txt),\n- [`class`](mixed-class2md.txt),\n- [`deck`](mixed-deck2md.txt),\n- [`deity`](mixed-deity2md.txt),\n- [`feat`](mixed-feat2md.txt),\n- [`hazard`](mixed-hazard2md.txt),\n- [`item`](mixed-item2md.txt),\n- [`monster`](mixed-monster2md.txt),\n- [`object`](mixed-object2md.txt),\n- [`race`](mixed-race2md.txt),\n- [`reward`](mixed-reward2md.txt),\n- [`spell`](mixed-spell2md.txt),\n- [`subclass`](mixed-subclass2md.txt),\n- [`vehicle`](mixed-vehicle2md.txt)\n"
  },
  {
    "path": "examples/templates/tools5e/mixed/mixed-background2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-background\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n{#if resource.hasImages }{resource.showPortraitImage}{/if}\n\n{resource.text}{#if resource.hasMoreImages }\n\n{resource.showMoreImages}\n{/if}{#if resource.source }\n\n## Sources\n\n*{resource.source}*{/if}\n"
  },
  {
    "path": "examples/templates/tools5e/mixed/mixed-class2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-class\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n\n{#if resource.classProgression }\n{resource.classProgression}\n\n{/if}\n## Hit Points\n{#if resource.hasImages }{resource.showPortraitImage}{/if}\n\n{#if resource.hitDice }\n- **Hit Dice**: 1d{resource.hitDice} per {resource.name} level\n- **Hit Points at First Level:** {resource.hitDice} + CON\n- **Hit Points at Higher Levels:** add {resource.hitRollAverage} OR 1d{resource.hitDice} + CON  (minimum of 1)\n{#else}\n- **Hit Dice**: *x* = hit dice specified in the sidekick's statblock (human, gnome, kobold, etc.)\n- **Hit Points at First Level:** *x* + CON\n- **Hit Points at Higher Levels:** add 1d*x* + CON (minimum of 1)\n{/if}{#if resource.hasMoreImages }\n\n{resource.showMoreImages}\n{/if}\n\n## Starting a {resource.name}\n\n{resource.startingEquipment}\n\n{#if resource.multiclassing }\n## Multiclassing {resource.name}\n\n{resource.multiclassing}\n{/if}\n\n{resource.text}{#if resource.source }\n\n## Sources\n\n*{resource.source}*{/if}\n"
  },
  {
    "path": "examples/templates/tools5e/mixed/mixed-deck2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-deck\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n\n{resource.text}\n\n## Cards\n\n{#for card in resource.cards}\n### {card.name}{#if card.face }\n{card.face.getEmbeddedLink(\"card\")}{/if}\n{card.text}\n\n{/for}{#if resource.source }\n\n## Sources\n\n*{resource.source}*{/if}\n"
  },
  {
    "path": "examples/templates/tools5e/mixed/mixed-deity2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-deity\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n{#if resource.image}\n{resource.image.getEmbeddedLink(\"symbol\")}{/if}\n\n{#if resource.altNames }\n- **Alternate Names**: {#each resource.altNames}{it}{#if it_hasNext}, {/if}{/each}\n{/if}{#if resource.alignment }\n- **Alignment**: {resource.alignment}\n{/if}{#if resource.category }\n- **Category**: {resource.category}\n{/if}{#if resource.domains }\n- **Domains**: {resource.domains}\n{/if}{#if resource.pantheon }\n- **Pantheon**: {resource.pantheon}\n{/if}{#if resource.province }\n- **Province**: {resource.province}\n{/if}{#if resource.symbol }\n- **Symbol**: {resource.symbol}\n{/if}\n\n{resource.text}{#if resource.source }\n\n## Sources\n\n*{resource.source}*{/if}\n"
  },
  {
    "path": "examples/templates/tools5e/mixed/mixed-feat2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-feat\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name} \n{#if resource.hasImages }{resource.showPortraitImage}{/if}\n\n{#if resource.level || resource.prerequisite}\n{#if resource.prerequisite}\n***Prerequisites*** {resource.prerequisite}\n{/if}\n{#if resource.level}\n***Level*** {resource.level}\n{/if}\n\n{/if}\n{resource.text}{#if resource.hasMoreImages }\n\n{resource.showMoreImages}\n{/if}{#if resource.source }\n\n## Sources\n\n*{resource.source}*{/if}\n"
  },
  {
    "path": "examples/templates/tools5e/mixed/mixed-hazard2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-hazard\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n{#if resource.hazardType }*{resource.hazardType}*  \n{/if}{#if resource.hasImages }{resource.showPortraitImage}{/if}{#if resource.text }\n\n{resource.text}\n{#else if resource.hasMoreImages }\n\n{resource.showMoreImages}\n{/if}{#if resource.source }\n\n## Sources\n\n*{resource.source}*{/if}\n"
  },
  {
    "path": "examples/templates/tools5e/mixed/mixed-item2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-item\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n{#if resource.detail }*{resource.detail}*  \n{/if}{#if resource.hasImages }{resource.showPortraitImage}{/if}\n\n{#if resource.armorClass }\n- **Armor Class**: {resource.armorClass}\n{#else if resource.damage }{#if resource.damage2h }\n- **Damage**:\n  - One-handed: {resource.damage}\n  - Two-handed: {resource.damage2h}\n{#else}\n- **Damage**: {resource.damage}\n{/if}{#if resource.range }\n- **Range**: {resource.range}\n{/if}{/if}{#if resource.properties }\n- **Properties**: {resource.properties}\n{/if}{#if resource.strengthRequirement }\n- **Strength**: Requires {resource.strengthRequirement} STR.\n{/if}{#if resource.stealthPenalty }\n- **Stealth**: The wearer has disadvantage on Stealth (DEX) checks.\n{/if}\n- **Cost**: {#if resource.cost }{resource.cost}{#else}⏤{/if}\n- **Weight**: {#if resource.weight }{resource.weight} lbs.{#else}⏤{/if}\n{#if resource.text }\n\n{resource.text}\n{/if}{#if resource.hasMoreImages }\n\n{resource.showMoreImages}\n{/if}{#if resource.variants }\n\n## Variants\n\n{resource.variantSectionLinks}\n{#for variant in resource.variants}\n\n### {variant.name}\n\n{#if variant.prerequisite}\n- **Prerequisites**: {variant.prerequisite}\n{/if}{#if variant.armorClass }\n- **Armor Class**: {variant.armorClass}\n{#else if variant.damage }{#if variant.damage2h }\n- **Damage**:\n  - One-handed: {variant.damage}\n  - Two-handed: {variant.damage2h}\n{#else}\n- **Damage**: {variant.damage}\n{/if}{#if variant.range }\n- **Range**: {variant.range}\n{/if}{/if}{#if variant.properties }\n- **Properties**: {variant.properties}\n{/if}{#if variant.strengthRequirement }\n- **Strength**: Requires {variant.strengthRequirement} STR.\n{/if}{#if variant.stealthPenalty }\n- **Stealth**: The wearer has disadvantage on Stealth (DEX) checks.\n{/if}{#if variant.cost }\n- **Cost**: {variant.cost}\n{/if}{#if variant.weight }\n- **Weight**: {variant.weight} lbs.\n{/if}{/for}\n{/if}{#if resource.source }\n\n## Sources\n\n*{resource.source}*{/if}\n\n"
  },
  {
    "path": "examples/templates/tools5e/mixed/mixed-monster2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-monster\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\nstatblock: true\nstatblock-link: \"#^statblock\"\n{resource.5eInitiativeYaml}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n\n{#if resource.description }\n{#if resource.hasImages }{resource.showPortraitImage}{/if}\n{resource.description}\n{#if resource.hasMoreImages }\n\n{resource.showMoreImages}\n{/if}\n{#else if resource.hasImages }\n{resource.showAllImages}\n\n{/if}{#if resource.hasSections }\n\n## Statblock\n{/if}\n\n```ad-statblock\ntitle: {resource.name}{#if resource.token}\n![{resource.token.title}]({resource.token.vaultPath}#token){/if}\n*{resource.size} {resource.fullType}, {resource.alignment}*\n\n- **Armor Class** {#if resource.ac }{resource.ac} {/if}{#if resource.acText }({resource.acText}){/if}\n- **Hit Points** {resource.hp} {#if resource.hitDice }({resource.hitDice}){/if} {#if resource.hpText }({resource.hpText}){/if}\n- **Speed** {resource.speed}\n\n|STR|DEX|CON|INT|WIS|CHA|\n|:---:|:---:|:---:|:---:|:---:|:---:|\n|{resource.scores}|\n\n- **Proficiency Bonus** {resource.pb}\n- **Saving Throws** {#if resource.savingThrows }{resource.savingThrows}{#else}⏤{/if}\n- **Skills** {#if resource.skills }{resource.skills}{#else}⏤{/if}\n- **Senses** {#if resource.senses }{resource.senses}, {/if}passive Perception {resource.passive}\n{#if resource.vulnerable }\n- **Damage Vulnerabilities** {resource.vulnerable}\n{/if}{#if resource.resist}\n- **Damage Resistances** {resource.resist}\n{/if}{#if resource.immune}\n- **Damage Immunities** {resource.immune}\n{/if}{#if resource.conditionImmune}\n- **Condition Immunities** {resource.conditionImmune}\n{/if}\n- **Languages** {#if resource.languages }{resource.languages}{#else}—{/if}\n- **Challenge** {resource.cr}\n{#if resource.trait}\n\n## Traits\n{#for trait in resource.trait}\n\n{#if trait.name }***{trait.name}.*** {/if}{trait.desc}\n{/for}{/if}{#if resource.action}\n\n## Actions\n{#for action in resource.action}\n\n{#if action.name }***{action.name}.*** {/if}{action.desc}\n{/for}{/if}{#if resource.bonusAction}\n\n## Bonus Actions\n{#for bonusAction in resource.bonusAction}\n\n{#if bonusAction.name }***{bonusAction.name}.*** {/if}{bonusAction.desc}\n{/for}{/if}{#if resource.reaction}\n\n## Reactions\n{#for reaction in resource.reaction}\n\n{#if reaction.name }***{reaction.name}.*** {/if}{reaction.desc}\n{/for}{/if}{#if resource.legendary}\n\n## Legendary Actions\n{#for legendary in resource.legendary}\n\n{#if legendary.name }***{legendary.name}.*** {/if}{legendary.desc}\n{/for}{/if}{#if resource.legendaryGroup}{#for group in resource.legendaryGroup}\n\n## {group.name}\n\n{group.desc}\n{/for}{/if}\n```\n^statblock\n{#if resource.environment }\n\n## Environment\n\n{resource.environment}\n{/if}{#if resource.source }\n\n## Sources\n\n*{resource.source}*{/if}\n"
  },
  {
    "path": "examples/templates/tools5e/mixed/mixed-object2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-object\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n{#if resource.text }\n{#if resource.hasImages }{resource.showPortraitImage}{/if}\n\n{resource.text}\n{#if resource.hasMoreImages }\n\n{resource.showMoreImages}\n{/if}\n{#else if resource.hasImages }\n{resource.showAllImages}\n\n{/if}{#if resource.hasSections }\n## Statblock\n\n{/if}\n```ad-statblock\ntitle: {resource.name}{#if resource.token}\n![{resource.token.title}]({resource.token.vaultPath}#token){/if}\n*{resource.size} {resource.objectType}{#if resource.creatureType } ({resource.creatureType}){/if}*\n\n- **Armor Class** {#if resource.ac }{resource.ac} {/if}{#if resource.acText }({resource.acText}){/if}\n- **Hit Points** {resource.hp or ' '} {#if resource.hpText }({resource.hpText}){/if}\n- **Speed** {resource.speed}\n\n|STR|DEX|CON|INT|WIS|CHA|\n|:---:|:---:|:---:|:---:|:---:|:---:|\n|{resource.scores}|\n\n{#if resource.senses }\n- **Senses** {resource.senses}\n{/if}{#if resource.vulnerable }\n- **Damage Vulnerabilities** {resource.vulnerable}\n{/if}{#if resource.resist}\n- **Damage Resistances** {resource.resist}\n{/if}{#if resource.immune}\n- **Damage Immunities** {resource.immune}\n{/if}{#if resource.conditionImmune}\n- **Condition Immunities** {resource.conditionImmune}\n{/if}\n{#if resource.action}\n\n## Actions\n{#for action in resource.action}\n\n{#if action.name }***{action.name}.*** {/if}{action.desc}\n{/for}{/if}\n```\n^statblock{#if resource.source }\n\n## Sources\n\n*{resource.source}*{/if}\n"
  },
  {
    "path": "examples/templates/tools5e/mixed/mixed-race2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-race\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n{#if resource.hasImages }{resource.showPortraitImage}{/if}\n\n- **Ability Scores**: {resource.ability}\n{#if resource.type}\n- **Type**: {resource.type}\n{/if}\n- **Size**: {resource.size}\n- **Speed**: {resource.speed}\n{#if resource.spellcasting}\n- **Spellcasting**: {resource.spellcasting}\n{/if}\n\n## Traits\n\n{resource.traits}\n{#if resource.description}\n\n## Description\n\n{resource.description}\n\n{/if}{#if resource.hasMoreImages }\n\n{resource.showMoreImages}\n{/if}{#if resource.source }\n\n## Sources\n\n*{resource.source}*{/if}\n"
  },
  {
    "path": "examples/templates/tools5e/mixed/mixed-reward2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-reward\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n{#if resource.detail }*{resource.detail}*  \n{/if}{#if resource.hasImages }{resource.showPortraitImage}{/if}\n{#if resource.signatureSpells }\n\n- **Signature Spells**: {resource.signatureSpells}\n{/if}{#if resource.text }\n\n{resource.text}\n{/if}{#if resource.source }\n\n## Sources\n\n*{resource.source}*{/if}\n"
  },
  {
    "path": "examples/templates/tools5e/mixed/mixed-spell2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-spell\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n%%-- Embedded content starts on the next line. --%%\n*{resource.level}, {resource.school}{#if resource.ritual} (ritual){/if}*  \n{#if resource.hasImages }{resource.showPortraitImage}{/if}\n\n- **Casting time:** {resource.time}{#if resource.ritual} unless cast as a ritual{/if}\n- **Range:** {resource.range}\n- **Components:** {resource.components}\n- **Duration:** {resource.duration}\n\n{resource.text}\n\n{#if resource.hasSections }\n## Summary\n\n{/if}{#if resource.hasMoreImages }\n\n{resource.showMoreImages}\n{/if}{#if resource.references }\n**References**:\n\n{#each resource.references}\n- {it}\n{/each}\n{/if}{#if resource.source }\n\n## Sources\n\n*{resource.source}*{/if}\n"
  },
  {
    "path": "examples/templates/tools5e/mixed/mixed-subclass2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-class\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n*{resource.parentClassLink}{#if resource.subclassTitle}: {resource.subclassTitle}{/if}*  \n\n{#if resource.classProgression }\n{resource.classProgression}\n\n{/if}\n{#if resource.hasImages }{resource.showPortraitImage}{/if}\n\n{resource.text}{#if resource.source }\n\n## Sources\n\n*{resource.source}*{/if}\n"
  },
  {
    "path": "examples/templates/tools5e/mixed/mixed-vehicle2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-vehicle\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n%%-- Embedded content starts on the next line. --%%\n*Source: {resource.source}*  \n{#if resource.text && !resource.isObject }{! ----- Fluff for non-OBJECT vehicles ----- !}\n{#if resource.hasImages }{resource.showPortraitImage}{/if}\n\n{resource.text}\n{#if resource.hasMoreImages }\n\n{resource.showMoreImages}\n{/if}\n{#else if resource.hasImages }{! ----- Fluff images for OBJECT vehicles (or no text) ----- !}\n\n{resource.showAllImages}\n\n{/if}{#if !resource.isObject && resource.hasSections }\n## Statblock\n\n{/if}\n```ad-statblock\ntitle: {resource.name}{#if resource.token}\n![{resource.token.title}]({resource.token.vaultPath}#token){/if}\n*{resource.sizeDimension}; {resource.terrain}*\n{#if resource.shipCrewCargoPace}\n\n{resource.shipCrewCargoPace}\n{/if}{#if resource.isObject }{! ----- BEGIN OBJECT (type) ----- !}\n{#if resource.scores}\n\n|STR|DEX|CON|INT|WIS|CHA|\n|:---:|:---:|:---:|:---:|:---:|:---:|\n|{resource.scores}|\n\n{/if}{#if resource.immuneResist }\n{resource.immuneResist}\n{/if}{#if resource.text }\n\n{resource.text}\n\n{/if}{#if resource.shipSections}{#each resource.shipSections}\n\n{it}\n{/each}{/if}\n{#else if resource.isCreature }{! ----- BEGIN CREATURE (type) ----- !}\n{#if resource.scores}\n\n|STR|DEX|CON|INT|WIS|CHA|\n|:---:|:---:|:---:|:---:|:---:|:---:|\n|{resource.scores}|\n\n{/if}{#if resource.immuneResist }\n{resource.immuneResist}\n{/if}{#if resource.shipSections}{#each resource.shipSections}\n\n{it}\n{/each}{/if}\n{#else if resource.isWarMachine }{! ----- BEGIN INFERNAL WAR MACHINE (type) ----- !}\n{#if resource.scores}\n\n|STR|DEX|CON|INT|WIS|CHA|\n|:---:|:---:|:---:|:---:|:---:|:---:|\n|{resource.scores}|\n\n{/if}{#if resource.immuneResist }\n{resource.immuneResist}\n{/if}{#if resource.shipSections}{#each resource.shipSections}\n\n{it}\n{/each}{/if}\n{#else if resource.isSpelljammer }{! ----- BEGIN SPELLJAMMER (type) ----- !}\n{#if resource.shipSections}{#each resource.shipSections}\n\n{it}\n{/each}{/if}\n{#else if resource.isShip }{! ----- BEGIN SHIP (type) ----- !}\n{#if resource.scores}\n\n|STR|DEX|CON|INT|WIS|CHA|\n|:---:|:---:|:---:|:---:|:---:|:---:|\n|{resource.scores}|\n\n{/if}{#if resource.immuneResist }\n{resource.immuneResist}\n{/if}{#if resource.action}\n\n## Actions\n{#each resource.action}\n\n{it}\n{/each}{/if}{#if resource.shipSections}{#each resource.shipSections}\n\n{it}\n{/each}{/if}\n{/if}{! END SHIP (type) !}\n```\n^statblock{#if resource.source }\n\n## Sources\n\n*{resource.source}*{/if}\n"
  },
  {
    "path": "examples/templates/tools5e/monster2md-2024.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-monster\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}\n{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n{#if resource.conditionImmune}\nconditionImmunities:\n{#for condition in resource.conditionImmune.split(\", ?\")}\n- {condition}\n{/for}\n{/if}\n---\n# {resource.name}\n*Source: {resource.source}*  \n\n{#if resource.description }\n{resource.description}\n\n{/if}\n```ad-statblock\ntitle: {resource.name}{#if resource.token}\n![{resource.token.title}]({resource.token.vaultPath}#token){/if}\n*{resource.size.capitalized} {resource.fullType.capitalized}, {resource.alignment.capitalized}*\n\n- **Armor Class** {#if resource.ac }{resource.ac} {/if}{#if resource.acText }({resource.acText}){/if}\n- **Hit Points** {resource.hp} {#if resource.hitDice }({resource.hitDice}){/if} {#if resource.hpText }({resource.hpText}){/if}\n- **Speed** {resource.speed}\n\n|   |   |  MOD | SAVE |\n|:--|:-:|:----:|:----:|\n|Str| {resource.scores.strStat} | {resource.scores.strMod} | {resource.savesSkills.saveOrDefault.strength} |\n|Int| {resource.scores.intStat} | {resource.scores.intMod} | {resource.savesSkills.saveOrDefault.intelligence} |\n|   |   |  MOD | SAVE |\n|:--|:-:|:----:|:----:|\n|Dex| {resource.scores.dexStat} | {resource.scores.dexMod} | {resource.savesSkills.saveOrDefault.dexterity} |\n|Wis| {resource.scores.wisStat} | {resource.scores.wisMod} | {resource.savesSkills.saveOrDefault.wisdom} |\n|   |   |  MOD | SAVE |\n|:--|:-:|:----:|:----:|\n|Con| {resource.scores.conStat} | {resource.scores.conMod} | {resource.savesSkills.saveOrDefault.constitution} |\n|Cha| {resource.scores.chaStat} | {resource.scores.chaMod} | {resource.savesSkills.saveOrDefault.charisma} |\n\n\n- **Proficiency Bonus** {resource.pb}\n- **Saving Throws** {#if resource.savingThrows }{resource.savingThrows.capitalizedList}{#else}⏤{/if}\n- **Skills** {#if resource.skills }{resource.skills.capitalizedList}{#else}⏤{/if}\n{#if resource.vulnerable }\n- **Damage Vulnerabilities** {resource.vulnerable.capitalizedList}\n{/if}{#if resource.resist}\n- **Damage Resistances** {resource.resist.capitalizedList}\n{/if}{#if resource.immune}\n- **Damage Immunities** {resource.immune.capitalizedList}\n{/if}{#if resource.conditionImmune}\n- **Condition Immunities** {resource.conditionImmune.capitalizedList}\n{/if}{#if resource.gear}\n- **Gear** {resource.gear.join(\", \")}\n{/if}\n- **Senses** {#if resource.senses }{resource.senses.capitalizedList}, {/if}Passive Perception {resource.passive}\n- **Languages** {#if resource.languages }{resource.languages.capitalizedList}{#else}—{/if}\n- **Challenge** {resource.cr}\n{#if resource.trait}\n\n## Traits\n{#for trait in resource.trait}\n\n{#if trait.name }***{trait.name}.*** {/if}{trait.desc}\n{/for}{/if}{#if resource.action}\n\n## Actions\n{#for action in resource.action}\n\n{#if action.name }***{action.name}.*** {/if}{action.desc}\n{/for}{/if}{#if resource.bonusAction}\n\n## Bonus Actions\n{#for bonusAction in resource.bonusAction}\n\n{#if bonusAction.name }***{bonusAction.name}.*** {/if}{bonusAction.desc}\n{/for}{/if}{#if resource.reaction}\n\n## Reactions\n{#for reaction in resource.reaction}\n\n{#if reaction.name }***{reaction.name}.*** {/if}{reaction.desc}\n{/for}{/if}{#if resource.legendary}\n\n## Legendary Actions\n{#for legendary in resource.legendary}\n\n{#if legendary.name }***{legendary.name}.*** {/if}{legendary.desc}\n{/for}{/if}{#if resource.legendaryGroup}{#for group in resource.legendaryGroup}\n\n## {group.key}\n\n{group.desc}\n{/for}{/if}\n```\n^statblock\n{#if resource.environment }\n\n## Environment\n\n{resource.environment}\n{/if}\n"
  },
  {
    "path": "examples/templates/tools5e/monster2md-scores.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-monster\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}\n{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n{#if resource.conditionImmune}\nconditionImmunities:\n{#for condition in resource.conditionImmune.split(\", ?\")}\n- \"{condition}\"\n{/for}\n{/if}\n---\n# {resource.name}\n*Source: {resource.source}*  \n\n{#if resource.description }\n{resource.description}\n\n{/if}\n```ad-statblock\ntitle: {resource.name}{#if resource.token}\n![{resource.token.title}]({resource.token.vaultPath}#token){/if}\n*{resource.size} {resource.fullType}, {resource.alignment}*\n\n- **Armor Class** {#if resource.ac }{resource.ac} {/if}{#if resource.acText }({resource.acText}){/if}\n- **Hit Points** {resource.hp} {#if resource.hitDice }({resource.hitDice}){/if} {#if resource.hpText }({resource.hpText}){/if}\n- **Speed** {resource.speed}\n\n- STR: {resource.scores.str} `dice: 1d20 {resource.scores.strMod}`\n- DEX: {resource.scores.dex} `dice: 1d20 {resource.scores.dexMod}` \n- CON: {resource.scores.con} `dice: 1d20 {resource.scores.conMod}` \n- INT: {resource.scores.int} `dice: 1d20 {resource.scores.intMod}` \n- WIS: {resource.scores.wis} `dice: 1d20 {resource.scores.wisMod}`\n- CHA: {resource.scores.cha} `dice: 1d20 {resource.scores.chaMod}`\n\n- **Proficiency Bonus** {resource.pb}\n- **Saving Throws** {#if resource.savingThrows }{resource.savingThrows}{#else}⏤{/if}\n- **Skills** {#if resource.skills }{resource.skills}{#else}⏤{/if}\n- **Senses** {#if resource.senses }{resource.senses}, {/if}passive Perception {resource.passive}\n{#if resource.vulnerable }\n- **Damage Vulnerabilities** {resource.vulnerable}\n{/if}{#if resource.resist}\n- **Damage Resistances** {resource.resist}\n{/if}{#if resource.immune}\n- **Damage Immunities** {resource.immune}\n{/if}{#if resource.conditionImmune}\n- **Condition Immunities** {resource.conditionImmune}\n{/if}{#if resource.gear}\n- **Gear** {resource.gear.join(\", \")}\n{/if}\n- **Languages** {#if resource.languages }{resource.languages}{#else}—{/if}\n- **Challenge** {resource.cr}\n{#if resource.trait}\n\n## Traits\n{#for trait in resource.trait}\n\n{#if trait.name }***{trait.name}.*** {/if}{trait.desc}\n{/for}{/if}{#if resource.action}\n\n## Actions\n{#for action in resource.action}\n\n{#if action.name }***{action.name}.*** {/if}{action.desc}\n{/for}{/if}{#if resource.bonusAction}\n\n## Bonus Actions\n{#for bonusAction in resource.bonusAction}\n\n{#if bonusAction.name }***{bonusAction.name}.*** {/if}{bonusAction.desc}\n{/for}{/if}{#if resource.reaction}\n\n## Reactions\n{#for reaction in resource.reaction}\n\n{#if reaction.name }***{reaction.name}.*** {/if}{reaction.desc}\n{/for}{/if}{#if resource.legendary}\n\n## Legendary Actions\n{#for legendary in resource.legendary}\n\n{#if legendary.name }***{legendary.name}.*** {/if}{legendary.desc}\n{/for}{/if}{#if resource.legendaryGroup}{#for group in resource.legendaryGroup}\n\n## {group.key}\n\n{group.desc}\n{/for}{/if}\n```\n^statblock\n{#if resource.environment }\n\n## Environment\n\n{resource.environment}\n{/if}\n"
  },
  {
    "path": "examples/templates/tools5e/monster2md-yamlStatblock-body.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-monster\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\nstatblock: inline\nstatblock-link: \"#^statblock\"\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# [{resource.name}]({resource.vaultPath})\n*Source: {resource.source}*  \n\n{#if resource.description }\n{resource.description}\n\n{/if}\n```statblock\n{resource.5eStatblockYaml}\n```\n^statblock\n{#if resource.environment }\n\n## Environment\n\n{resource.environment}\n{/if}\n"
  },
  {
    "path": "examples/templates/tools5e/monster2md-yamlStatblock-header.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-monster\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\nstatblock: true\nstatblock-link: \"#^statblock\"\n{resource.5eInitiativeYamlNoSource}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n*Source: {resource.source}*  \n\n{#if resource.description }\n{resource.description}\n\n{/if}\n```ad-statblock\ntitle: {resource.name}{#if resource.token}\n![{resource.token.title}]({resource.token.vaultPath}#token){/if}\n*{resource.size} {resource.fullType}, {resource.alignment}*\n\n- **Armor Class** {#if resource.ac }{resource.ac} {/if}{#if resource.acText }({resource.acText}){/if}\n- **Hit Points** {resource.hp} {#if resource.hitDice }({resource.hitDice}){/if} {#if resource.hpText }({resource.hpText}){/if}\n- **Speed** {resource.speed}\n\n|STR|DEX|CON|INT|WIS|CHA|\n|:---:|:---:|:---:|:---:|:---:|:---:|\n|{resource.scoreString}|\n\n- **Proficiency Bonus** {resource.pb}\n- **Saving Throws** {#if resource.savingThrows }{resource.savingThrows}{#else}⏤{/if}\n- **Skills** {#if resource.skills }{resource.skills}{#else}⏤{/if}\n- **Senses** {#if resource.senses }{resource.senses}, {/if}passive Perception {resource.passive}\n{#if resource.vulnerable }\n- **Damage Vulnerabilities** {resource.vulnerable}\n{/if}{#if resource.resist}\n- **Damage Resistances** {resource.resist}\n{/if}{#if resource.immune}\n- **Damage Immunities** {resource.immune}\n{/if}{#if resource.conditionImmune}\n- **Condition Immunities** {resource.conditionImmune}\n{/if}{#if resource.gear}\n- **Gear** {resource.gear.join(\", \")}\n{/if}\n- **Languages** {#if resource.languages }{resource.languages}{#else}—{/if}\n- **Challenge** {resource.cr}\n{#if resource.trait}\n\n## Traits\n{#for trait in resource.trait}\n\n{#if trait.name }***{trait.name}.*** {/if}{trait.desc}\n{/for}{/if}{#if resource.action}\n\n## Actions\n{#for action in resource.action}\n\n{#if action.name }***{action.name}.*** {/if}{action.desc}\n{/for}{/if}{#if resource.bonusAction}\n\n## Bonus Actions\n{#for bonusAction in resource.bonusAction}\n\n{#if bonusAction.name }***{bonusAction.name}.*** {/if}{bonusAction.desc}\n{/for}{/if}{#if resource.reaction}\n\n## Reactions\n{#for reaction in resource.reaction}\n\n{#if reaction.name }***{reaction.name}.*** {/if}{reaction.desc}\n{/for}{/if}{#if resource.legendary}\n\n## Legendary Actions\n{#for legendary in resource.legendary}\n\n{#if legendary.name }***{legendary.name}.*** {/if}{legendary.desc}\n{/for}{/if}{#if resource.legendaryGroup}{#for group in resource.legendaryGroup}\n\n## {group.name}\n\n{group.desc}\n{/for}{/if}\n```\n^statblock\n{#if resource.environment }\n\n## Environment\n\n{resource.environment}\n{/if}\n"
  },
  {
    "path": "examples/templates/tools5e/object2md-yamlStatblock-body.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-object\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\nstatblock: inline\nstatblock-link: \"#^statblock\"\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n*Source: {resource.source}*  \n\n{#if resource.text }\n{#if resource.fluffImages && resource.fluffImages.size > 0 }{#let first=resource.fluffImages.get(0)}\n![{first.title}]({first.vaultPath}#right)  \n{/let}{/if}\n{resource.text}\n{#each resource.fluffImages}{#if it_index != 0 }\n![{it.title}]({it.vaultPath}#center)  \n{/if}{/each}\n{#else}\n{#each resource.fluffImages}\n![{it.title}]({it.vaultPath}#center)  \n{/each}\n{/if}{#if resource.hasSections }\n## Statblock\n\n{/if}\n```statblock\n{resource.5eStatblockYaml}\n```\n^statblock\n\n"
  },
  {
    "path": "examples/templates/tools5e/object2md-yamlStatblock-header.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-object\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\nstatblock: true\nstatblock-link: \"#^statblock\"\n{resource.5eInitiativeYaml}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n*Source: {resource.source}*  \n\n{#if resource.text }\n{#if resource.fluffImages && resource.fluffImages.size > 0 }{#let first=resource.fluffImages.get(0)}\n![{first.title}]({first.vaultPath}#right)  \n{/let}{/if}\n{resource.text}\n{#each resource.fluffImages}{#if it_index != 0 }\n![{it.title}]({it.vaultPath}#center)  \n{/if}{/each}\n{#else}\n{#each resource.fluffImages}\n![{it.title}]({it.vaultPath}#center)  \n{/each}\n{/if}{#if resource.hasSections }\n## Statblock\n\n{/if}\n```ad-statblock\ntitle: {resource.name}{#if resource.token}\n![{resource.token.title}]({resource.token.vaultPath}#token){/if}\n*{resource.size} {resource.objectType}{#if resource.creatureType } ({resource.creatureType}){/if}*\n\n- **Armor Class** {#if resource.ac }{resource.ac} {/if}{#if resource.acText }({resource.acText}){/if}\n- **Hit Points** {resource.hp or ' '} {#if resource.hpText }({resource.hpText}){/if}\n- **Speed** {resource.speed}\n\n|STR|DEX|CON|INT|WIS|CHA|\n|:---:|:---:|:---:|:---:|:---:|:---:|\n|{resource.scores}|\n\n{#if resource.senses }\n- **Senses** {resource.senses}\n{/if}{#if resource.vulnerable }\n- **Damage Vulnerabilities** {resource.vulnerable}\n{/if}{#if resource.resist}\n- **Damage Resistances** {resource.resist}\n{/if}{#if resource.immune}\n- **Damage Immunities** {resource.immune}\n{/if}{#if resource.conditionImmune}\n- **Condition Immunities** {resource.conditionImmune}\n{/if}\n{#if resource.action}\n\n## Actions\n{#for action in resource.action}\n\n{#if action.name }***{action.name}.*** {/if}{action.desc}\n{/for}{/if}\n```\n^statblock\n\n"
  },
  {
    "path": "jreleaser.yml",
    "content": "environment:\n  properties:\n    nativeImageDir: target/jreleaser/assemble/native-archive/archive\n    uberJarDir: target\n\nproject:\n  name: ttrpg-convert-cli\n  description: Utility to convert 5eTools and Pf2eTools JSON data into Markdown\n  longDescription: |\n    This utility works with json sources and materials from the 5etools mirror to create\n    linked and formatted markdown. The resulting markdown will include backgrounds,\n    classes, deities, feats, items, monsters, races, and spells from a filtered list\n    of sources (constrained to those you specify == those you own). Books and adventures\n    can also be explicitly included. Generated content contains Obsidian-specific\n    formatting for some plugins (admonitions, dice roller, TTRPG Statblocks, etc.),\n    and is cross-linked and aliased for ease of reference.\n  links:\n    homepage: https://github.com/ebullient/ttrpg-convert-cli\n    documentation: https://github.com/ebullient/ttrpg-convert-cli#readme\n    license: https://github.com/ebullient/ttrpg-convert-cli/LICENSE\n  authors:\n    - \"ebullient (Erin Schnabel)\"\n  license: Apache-2.0\n  inceptionYear: 2022\n  stereotype: CLI\n  java:\n    groupId: dev.ebullient\n    version: 21\n    multiProject: false\n  tags:\n    - \"quarkus\"\n    - \"cli\"\n    - \"5e-tools\"\n    - \"pf2e-tools\"\n    - \"ttrpg\"\n    - \"java\"\n    - \"obsidian-md\"\n\nassemble:\n  # Archives for native images\n  archive:\n    native-archive:\n      active: ALWAYS\n      exported: false\n      attachPlatform: true\n      archiveName: \"{{projectName}}-{{projectVersion}}\"\n      formats:\n        - ZIP\n        - TGZ\n      fileSets:\n        - input: \".\"\n          includes:\n            - \"LICENSE\"\n            - \"README.md\"\n        - input: \"target/\"\n          includes:\n            - \"ttrpg-convert{.exe,}\"\n          output: \"bin\"\n\nchecksum:\n  individual: true\n\ndistributions:\n  uber-jar:\n    type: SINGLE_JAR\n    artifacts:\n      - path: '{{uberJarDir}}/{{projectName}}-{{projectVersion}}-runner.jar'\n\n  native-archive:\n    type: NATIVE_IMAGE\n    executable:\n      name: ttrpg-convert\n      windowsExtension: exe\n    artifacts:\n      - path: '{{nativeImageDir}}/{{projectName}}-{{projectVersion}}-linux-x86_64.zip'\n        platform: linux-x86_64\n      - path: '{{nativeImageDir}}/{{projectName}}-{{projectVersion}}-windows-x86_64.zip'\n        platform: windows-x86_64\n      - path: '{{nativeImageDir}}/{{projectName}}-{{projectVersion}}-osx-x86_64.zip'\n        platform: osx-x86_64\n      - path: '{{nativeImageDir}}/{{projectName}}-{{projectVersion}}-osx-aarch_64.zip'\n        platform: osx-aarch_64\n    brew:\n      active: ALWAYS\n      continueOnError: true\n      formulaName: 'ttrpg-convert-cli'\n      multiPlatform: true\n      skipTemplates:\n        - README.md.tpl\n      repository:\n        commitMessage: '🔖  {{tagName}} {{projectName}}'\n\nrelease:\n  github:\n    changelog:\n      formatted: ALWAYS\n      format: \"- {{commitShortHash}} {{commitTitle}}\"\n      content: |\n        # Summary of changes\n\n        {{changelogChanges}}\n        {{changelogContributors}}\n      labelers:\n        - label: \"infra\"\n          title: \"regex:(🔧|👷)\"\n        - label: \"deps\"\n          title: \"Bump \"\n        - label: \"generated\"\n          title: \"🤖 \"\n        - label: \"release\"\n          title: \"🔖 \"\n      excludeLabels:\n        - \"infra\"\n        - \"deps\"\n        - \"generated\"\n        - \"release\"\n      hide:\n        contributors:\n          - \"ebullient\"\n          - \"Erin Schnabel\"\n          - github-actions\n          - GitHub\n          - \"[bot]\"\n    checksums: true\n    discussionCategoryName: Announcements\n    issues:\n      enabled: false\n    milestone:\n      close: false\n    overwrite: false\n    update:\n      enabled: true\n      sections:\n        - ASSETS\n    skipTag: true\n    sign: false\n    tagName: \"{{projectVersion}}\"\n"
  },
  {
    "path": "migration/json5e-cli-renameFiles-1.0.12.md",
    "content": "<%*\n// NOTE: There are a lot of files: this process can take awhile\n// It goes faster if you drop into safe mode and disable sync.\n\n// 1. Copy this file into your templates directory\n\n// 2. Update the path to match your compendium (parent bestiary, classes, deities, etc.)\nconst compendium = \"/compendium\";\n\n// 3. How many files to rename at a time?\n// There are ~4100 files in this list, only some of which will apply to your vault.\nconst limit = 50;\n\n// 4. Create a new/temporary note, and use the \"Templater: Open Insert Template Modal\"\n// to insert this template into the document. It will update the document \n// with the files that have been renamed. \n// It will emit an empty table when there are no files left to rename.\n\nvar f;\nlet count = 0;\n\ntR += \"| File | New Name |\\n\";\ntR += \"|------|----------|\\n\";\n\nasync function moveFile(path, oldname, newname) {\n    if (count > limit) {\n        return;\n    }\n    \n    const oldpath = `${compendium}/${path}/${oldname}`\n    const file = await window.app.metadataCache.getFirstLinkpathDest(oldpath, \"\");\n    \n    if (file) {\n        const newpath = `${compendium}/${path}/${newname}.md`\n        await this.app.fileManager.renameFile(\n            file,\n            newpath\n        );\n        tR += `| ${oldname} | ${newname} |\\n`;\n        \n        count++; // increment counter after moving something\n    }\n}\n\nawait moveFile(\"backgrounds\", \"anthropologist\", \"anthropologist-toa\");\nawait moveFile(\"backgrounds\", \"archaeologist\", \"archaeologist-toa\");\nawait moveFile(\"backgrounds\", \"astral-drifter\", \"astral-drifter-aag\");\nawait moveFile(\"backgrounds\", \"athlete\", \"athlete-mot\");\nawait moveFile(\"backgrounds\", \"augen-trust-spy\", \"augen-trust-spy-egw\");\nawait moveFile(\"backgrounds\", \"azorius-functionary\", \"azorius-functionary-ggr\");\nawait moveFile(\"backgrounds\", \"baldurs-gate-acolyte\", \"baldurs-gate-acolyte-bgdia\");\nawait moveFile(\"backgrounds\", \"baldurs-gate-charlatan\", \"baldurs-gate-charlatan-bgdia\");\nawait moveFile(\"backgrounds\", \"baldurs-gate-criminal\", \"baldurs-gate-criminal-bgdia\");\nawait moveFile(\"backgrounds\", \"baldurs-gate-entertainer\", \"baldurs-gate-entertainer-bgdia\");\nawait moveFile(\"backgrounds\", \"baldurs-gate-folk-hero\", \"baldurs-gate-folk-hero-bgdia\");\nawait moveFile(\"backgrounds\", \"baldurs-gate-guild-artisan\", \"baldurs-gate-guild-artisan-bgdia\");\nawait moveFile(\"backgrounds\", \"baldurs-gate-hermit\", \"baldurs-gate-hermit-bgdia\");\nawait moveFile(\"backgrounds\", \"baldurs-gate-noble\", \"baldurs-gate-noble-bgdia\");\nawait moveFile(\"backgrounds\", \"baldurs-gate-outlander\", \"baldurs-gate-outlander-bgdia\");\nawait moveFile(\"backgrounds\", \"baldurs-gate-sage\", \"baldurs-gate-sage-bgdia\");\nawait moveFile(\"backgrounds\", \"baldurs-gate-sailor\", \"baldurs-gate-sailor-bgdia\");\nawait moveFile(\"backgrounds\", \"baldurs-gate-soldier\", \"baldurs-gate-soldier-bgdia\");\nawait moveFile(\"backgrounds\", \"baldurs-gate-urchin\", \"baldurs-gate-urchin-bgdia\");\nawait moveFile(\"backgrounds\", \"black-fist-double-agent\", \"black-fist-double-agent-alcurseofstrahd\");\nawait moveFile(\"backgrounds\", \"boros-legionnaire\", \"boros-legionnaire-ggr\");\nawait moveFile(\"backgrounds\", \"caravan-specialist\", \"caravan-specialist-alelementalevil\");\nawait moveFile(\"backgrounds\", \"celebrity-adventurers-scion\", \"celebrity-adventurers-scion-ai\");\nawait moveFile(\"backgrounds\", \"city-watch-investigator-variant\", \"city-watch-investigator-variant-scag\");\nawait moveFile(\"backgrounds\", \"city-watch\", \"city-watch-scag\");\nawait moveFile(\"backgrounds\", \"clan-crafter\", \"clan-crafter-scag\");\nawait moveFile(\"backgrounds\", \"cloistered-scholar\", \"cloistered-scholar-scag\");\nawait moveFile(\"backgrounds\", \"cobalt-scholar-sage\", \"cobalt-scholar-sage-egw\");\nawait moveFile(\"backgrounds\", \"cormanthor-refugee\", \"cormanthor-refugee-alrageofdemons\");\nawait moveFile(\"backgrounds\", \"courtier\", \"courtier-scag\");\nawait moveFile(\"backgrounds\", \"dimir-operative\", \"dimir-operative-ggr\");\nawait moveFile(\"backgrounds\", \"dissenter\", \"dissenter-psa\");\nawait moveFile(\"backgrounds\", \"dragon-casualty\", \"dragon-casualty-alcurseofstrahd\");\nawait moveFile(\"backgrounds\", \"earthspur-miner\", \"earthspur-miner-alelementalevil\");\nawait moveFile(\"backgrounds\", \"faceless\", \"faceless-bgdia\");\nawait moveFile(\"backgrounds\", \"faction-agent\", \"faction-agent-scag\");\nawait moveFile(\"backgrounds\", \"failed-merchant\", \"failed-merchant-ai\");\nawait moveFile(\"backgrounds\", \"far-traveler\", \"far-traveler-scag\");\nawait moveFile(\"backgrounds\", \"feylost\", \"feylost-wbtw\");\nawait moveFile(\"backgrounds\", \"fisher\", \"fisher-gos\");\nawait moveFile(\"backgrounds\", \"gambler\", \"gambler-ai\");\nawait moveFile(\"backgrounds\", \"gate-urchin\", \"gate-urchin-alrageofdemons\");\nawait moveFile(\"backgrounds\", \"gate-warden-ua\", \"gate-warden-ua2022wondersofthemultiverse\");\nawait moveFile(\"backgrounds\", \"giant-foundling-ua\", \"giant-foundling-ua2022wondersofthemultiverse\");\nawait moveFile(\"backgrounds\", \"golgari-agent\", \"golgari-agent-ggr\");\nawait moveFile(\"backgrounds\", \"grinner\", \"grinner-egw\");\nawait moveFile(\"backgrounds\", \"gruul-anarch\", \"gruul-anarch-ggr\");\nawait moveFile(\"backgrounds\", \"harborfolk\", \"harborfolk-alelementalevil\");\nawait moveFile(\"backgrounds\", \"haunted-one\", \"haunted-one-vrgr\");\nawait moveFile(\"backgrounds\", \"hillsfar-merchant\", \"hillsfar-merchant-alrageofdemons\");\nawait moveFile(\"backgrounds\", \"hillsfar-smuggler\", \"hillsfar-smuggler-alrageofdemons\");\nawait moveFile(\"backgrounds\", \"house-agent\", \"house-agent-erlw\");\nawait moveFile(\"backgrounds\", \"inheritor\", \"inheritor-scag\");\nawait moveFile(\"backgrounds\", \"initiate\", \"initiate-psa\");\nawait moveFile(\"backgrounds\", \"inquisitor\", \"inquisitor-psi\");\nawait moveFile(\"backgrounds\", \"investigator\", \"investigator-vrgr\");\nawait moveFile(\"backgrounds\", \"iron-route-bandit\", \"iron-route-bandit-alcurseofstrahd\");\nawait moveFile(\"backgrounds\", \"izzet-engineer\", \"izzet-engineer-ggr\");\nawait moveFile(\"backgrounds\", \"knight-of-solamnia-ua\", \"knight-of-solamnia-ua2022heroesofkrynnrevisited\");\nawait moveFile(\"backgrounds\", \"knight-of-the-order\", \"knight-of-the-order-scag\");\nawait moveFile(\"backgrounds\", \"lorehold-student\", \"lorehold-student-scc\");\nawait moveFile(\"backgrounds\", \"luxonborn-acolyte\", \"luxonborn-acolyte-egw\");\nawait moveFile(\"backgrounds\", \"mage-of-high-sorcery-ua\", \"mage-of-high-sorcery-ua2022heroesofkrynnrevisited\");\nawait moveFile(\"backgrounds\", \"marine\", \"marine-gos\");\nawait moveFile(\"backgrounds\", \"mercenary-veteran\", \"mercenary-veteran-scag\");\nawait moveFile(\"backgrounds\", \"mulmaster-aristocrat\", \"mulmaster-aristocrat-alelementalevil\");\nawait moveFile(\"backgrounds\", \"myriad-operative-criminal\", \"myriad-operative-criminal-egw\");\nawait moveFile(\"backgrounds\", \"orzhov-representative\", \"orzhov-representative-ggr\");\nawait moveFile(\"backgrounds\", \"phlan-insurgent\", \"phlan-insurgent-alcurseofstrahd\");\nawait moveFile(\"backgrounds\", \"phlan-refugee\", \"phlan-refugee-alelementalevil\");\nawait moveFile(\"backgrounds\", \"plaintiff\", \"plaintiff-ai\");\nawait moveFile(\"backgrounds\", \"planar-philosopher-ua\", \"planar-philosopher-ua2022wondersofthemultiverse\");\nawait moveFile(\"backgrounds\", \"prismari-student\", \"prismari-student-scc\");\nawait moveFile(\"backgrounds\", \"quandrix-student\", \"quandrix-student-scc\");\nawait moveFile(\"backgrounds\", \"rakdos-cultist\", \"rakdos-cultist-ggr\");\nawait moveFile(\"backgrounds\", \"revelry-pirate-sailor\", \"revelry-pirate-sailor-egw\");\nawait moveFile(\"backgrounds\", \"rival-intern\", \"rival-intern-ai\");\nawait moveFile(\"backgrounds\", \"rune-carver-ua\", \"rune-carver-ua2022wondersofthemultiverse\");\nawait moveFile(\"backgrounds\", \"secret-identity\", \"secret-identity-alrageofdemons\");\nawait moveFile(\"backgrounds\", \"selesnya-initiate\", \"selesnya-initiate-ggr\");\nawait moveFile(\"backgrounds\", \"shade-fanatic\", \"shade-fanatic-alrageofdemons\");\nawait moveFile(\"backgrounds\", \"shipwright\", \"shipwright-gos\");\nawait moveFile(\"backgrounds\", \"silverquill-student\", \"silverquill-student-scc\");\nawait moveFile(\"backgrounds\", \"simic-scientist\", \"simic-scientist-ggr\");\nawait moveFile(\"backgrounds\", \"smuggler\", \"smuggler-gos\");\nawait moveFile(\"backgrounds\", \"stojanow-prisoner\", \"stojanow-prisoner-alcurseofstrahd\");\nawait moveFile(\"backgrounds\", \"ticklebelly-nomad\", \"ticklebelly-nomad-alcurseofstrahd\");\nawait moveFile(\"backgrounds\", \"trade-sheriff\", \"trade-sheriff-alrageofdemons\");\nawait moveFile(\"backgrounds\", \"urban-bounty-hunter\", \"urban-bounty-hunter-scag\");\nawait moveFile(\"backgrounds\", \"uthgardt-tribe-member\", \"uthgardt-tribe-member-scag\");\nawait moveFile(\"backgrounds\", \"vizier\", \"vizier-psa\");\nawait moveFile(\"backgrounds\", \"volstrucker-agent\", \"volstrucker-agent-egw\");\nawait moveFile(\"backgrounds\", \"waterdhavian-noble\", \"waterdhavian-noble-scag\");\nawait moveFile(\"backgrounds\", \"wildspacer\", \"wildspacer-aag\");\nawait moveFile(\"backgrounds\", \"witchlight-hand\", \"witchlight-hand-wbtw\");\nawait moveFile(\"backgrounds\", \"witherbloom-student\", \"witherbloom-student-scc\");\nawait moveFile(\"bestiary/aberration\", \"aberrant-spirit-4th-level-spell\", \"aberrant-spirit-4th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/aberration\", \"aberrant-spirit-5th-level-spell\", \"aberrant-spirit-5th-level-spell-tce\");\nawait moveFile(\"bestiary/aberration\", \"aberrant-spirit-6th-level-spell\", \"aberrant-spirit-6th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/aberration\", \"aberrant-spirit-7th-level-spell\", \"aberrant-spirit-7th-level-spell-tce\");\nawait moveFile(\"bestiary/aberration\", \"aberrant-spirit-8th-level-spell\", \"aberrant-spirit-8th-level-spell-tce\");\nawait moveFile(\"bestiary/aberration\", \"aberrant-spirit-9th-level-spell\", \"aberrant-spirit-9th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/aberration\", \"aboleth-spawn\", \"aboleth-spawn-crcotn\");\nawait moveFile(\"bestiary/aberration\", \"alyxian-aboleth\", \"alyxian-aboleth-crcotn\");\nawait moveFile(\"bestiary/aberration\", \"balhannoth\", \"balhannoth-mpmm\");\nawait moveFile(\"bestiary/aberration\", \"berbalang\", \"berbalang-mpmm\");\nawait moveFile(\"bestiary/aberration\", \"chaos-quadrapod\", \"chaos-quadrapod-ai\");\nawait moveFile(\"bestiary/aberration\", \"choker\", \"choker-mpmm\");\nawait moveFile(\"bestiary/aberration\", \"core-spawn-crawler\", \"core-spawn-crawler-egw\");\nawait moveFile(\"bestiary/aberration\", \"core-spawn-emissary\", \"core-spawn-emissary-egw\");\nawait moveFile(\"bestiary/aberration\", \"core-spawn-seer\", \"core-spawn-seer-egw\");\nawait moveFile(\"bestiary/aberration\", \"core-spawn-worm\", \"core-spawn-worm-egw\");\nawait moveFile(\"bestiary/aberration\", \"corrupted-giant-shark\", \"corrupted-giant-shark-crcotn\");\nawait moveFile(\"bestiary/aberration\", \"cosmic-horror\", \"cosmic-horror-bam\");\nawait moveFile(\"bestiary/aberration\", \"cranium-rat\", \"cranium-rat-mpmm\");\nawait moveFile(\"bestiary/aberration\", \"death-embrace\", \"death-embrace-crcotn\");\nawait moveFile(\"bestiary/aberration\", \"death-kiss\", \"death-kiss-mpmm\");\nawait moveFile(\"bestiary/aberration\", \"derro\", \"derro-mpmm\");\nawait moveFile(\"bestiary/aberration\", \"derro-savant\", \"derro-savant-mpmm\");\nawait moveFile(\"bestiary/aberration\", \"dolgaunt\", \"dolgaunt-erlw\");\nawait moveFile(\"bestiary/aberration\", \"dolgrim\", \"dolgrim-erlw\");\nawait moveFile(\"bestiary/aberration\", \"elder-brain-dragon\", \"elder-brain-dragon-ftd\");\nawait moveFile(\"bestiary/aberration\", \"elder-brain\", \"elder-brain-mpmm\");\nawait moveFile(\"bestiary/aberration\", \"esthetic\", \"esthetic-bam\");\nawait moveFile(\"bestiary/aberration\", \"eye-monger\", \"eye-monger-bam\");\nawait moveFile(\"bestiary/aberration\", \"eyedrake\", \"eyedrake-ftd\");\nawait moveFile(\"bestiary/aberration\", \"feyr\", \"feyr-bam\");\nawait moveFile(\"bestiary/aberration\", \"flying-horror\", \"flying-horror-ggr\");\nawait moveFile(\"bestiary/aberration\", \"gaj\", \"gaj-bam\");\nawait moveFile(\"bestiary/aberration\", \"gauth\", \"gauth-mpmm\");\nawait moveFile(\"bestiary/aberration\", \"gazer\", \"gazer-mpmm\");\nawait moveFile(\"bestiary/aberration\", \"gibberling\", \"gibberling-mabjov\");\nawait moveFile(\"bestiary/aberration\", \"gingwatzim\", \"gingwatzim-cm\");\nawait moveFile(\"bestiary/aberration\", \"gnome-ceremorph\", \"gnome-ceremorph-idrotf\");\nawait moveFile(\"bestiary/aberration\", \"gnome-squidling\", \"gnome-squidling-idrotf\");\nawait moveFile(\"bestiary/aberration\", \"goon-balloon\", \"goon-balloon-mcv1sc\");\nawait moveFile(\"bestiary/aberration\", \"greater-shadow-horror\", \"greater-shadow-horror-aitfr-thp\");\nawait moveFile(\"bestiary/aberration\", \"greater-star-spawn-emissary\", \"greater-star-spawn-emissary-vrgr\");\nawait moveFile(\"bestiary/aberration\", \"hangry-otyugh\", \"hangry-otyugh-awm\");\nawait moveFile(\"bestiary/aberration\", \"hashalaq-quori\", \"hashalaq-quori-erlw\");\nawait moveFile(\"bestiary/aberration\", \"ixitxachitl-cleric\", \"ixitxachitl-cleric-oota\");\nawait moveFile(\"bestiary/aberration\", \"ixitxachitl\", \"ixitxachitl-oota\");\nawait moveFile(\"bestiary/aberration\", \"kalaraq-quori\", \"kalaraq-quori-erlw\");\nawait moveFile(\"bestiary/aberration\", \"lesser-star-spawn-emissary\", \"lesser-star-spawn-emissary-vrgr\");\nawait moveFile(\"bestiary/aberration\", \"light-devourer\", \"light-devourer-crcotn\");\nawait moveFile(\"bestiary/aberration\", \"mind-flayer-psion\", \"mind-flayer-psion-vgm\");\nawait moveFile(\"bestiary/aberration\", \"mindwitness\", \"mindwitness-mpmm\");\nawait moveFile(\"bestiary/aberration\", \"morkoth\", \"morkoth-mpmm\");\nawait moveFile(\"bestiary/aberration\", \"neh-thalggu\", \"neh-thalggu-bam\");\nawait moveFile(\"bestiary/aberration\", \"neo-otyugh\", \"neo-otyugh-imr\");\nawait moveFile(\"bestiary/aberration\", \"neogi-hatchling\", \"neogi-hatchling-mpmm\");\nawait moveFile(\"bestiary/aberration\", \"neogi-hatchling-swarm\", \"neogi-hatchling-swarm-bam\");\nawait moveFile(\"bestiary/aberration\", \"neogi-master\", \"neogi-master-mpmm\");\nawait moveFile(\"bestiary/aberration\", \"neogi\", \"neogi-mpmm\");\nawait moveFile(\"bestiary/aberration\", \"neogi-pirate\", \"neogi-pirate-bam\");\nawait moveFile(\"bestiary/aberration\", \"neogi-void-hunter\", \"neogi-void-hunter-bam\");\nawait moveFile(\"bestiary/aberration\", \"neothelid\", \"neothelid-mpmm\");\nawait moveFile(\"bestiary/aberration\", \"phaerimm\", \"phaerimm-mabjov\");\nawait moveFile(\"bestiary/aberration\", \"psurlon\", \"psurlon-bam\");\nawait moveFile(\"bestiary/aberration\", \"psurlon-leader\", \"psurlon-leader-bam\");\nawait moveFile(\"bestiary/aberration\", \"psurlon-ringer\", \"psurlon-ringer-bam\");\nawait moveFile(\"bestiary/aberration\", \"puppeteer-parasite\", \"puppeteer-parasite-mcv1sc\");\nawait moveFile(\"bestiary/aberration\", \"reduced-threat-aboleth\", \"reduced-threat-aboleth-tftyp\");\nawait moveFile(\"bestiary/aberration\", \"reduced-threat-beholder\", \"reduced-threat-beholder-tftyp\");\nawait moveFile(\"bestiary/aberration\", \"reduced-threat-otyugh\", \"reduced-threat-otyugh-tftyp\");\nawait moveFile(\"bestiary/aberration\", \"scuttling-serpentmaw\", \"scuttling-serpentmaw-crcotn\");\nawait moveFile(\"bestiary/aberration\", \"shadow-horror\", \"shadow-horror-ggr\");\nawait moveFile(\"bestiary/aberration\", \"sharkbody-abomination\", \"sharkbody-abomination-egw\");\nawait moveFile(\"bestiary/aberration\", \"skittering-horror\", \"skittering-horror-ggr\");\nawait moveFile(\"bestiary/aberration\", \"skum\", \"skum-gos\");\nawait moveFile(\"bestiary/aberration\", \"slithering-bloodfin\", \"slithering-bloodfin-crcotn\");\nawait moveFile(\"bestiary/aberration\", \"sorrowfish\", \"sorrowfish-crcotn\");\nawait moveFile(\"bestiary/aberration\", \"star-spawn-grue\", \"star-spawn-grue-mpmm\");\nawait moveFile(\"bestiary/aberration\", \"star-spawn-hulk\", \"star-spawn-hulk-mpmm\");\nawait moveFile(\"bestiary/aberration\", \"star-spawn-larva-mage\", \"star-spawn-larva-mage-mpmm\");\nawait moveFile(\"bestiary/aberration\", \"star-spawn-mangler\", \"star-spawn-mangler-mpmm\");\nawait moveFile(\"bestiary/aberration\", \"star-spawn-seer\", \"star-spawn-seer-mpmm\");\nawait moveFile(\"bestiary/aberration\", \"swarm-of-cranium-rats\", \"swarm-of-cranium-rats-mpmm\");\nawait moveFile(\"bestiary/aberration\", \"swarm-of-sorrowfish\", \"swarm-of-sorrowfish-crcotn\");\nawait moveFile(\"bestiary/aberration\", \"tsucora-quori\", \"tsucora-quori-erlw\");\nawait moveFile(\"bestiary/aberration\", \"ulitharid\", \"ulitharid-mpmm\");\nawait moveFile(\"bestiary/aberration\", \"usagt\", \"usagt-aitfr-thp\");\nawait moveFile(\"bestiary/aberration\", \"vampiric-ixitxachitl-cleric\", \"vampiric-ixitxachitl-cleric-oota\");\nawait moveFile(\"bestiary/aberration\", \"vampiric-ixitxachitl\", \"vampiric-ixitxachitl-oota\");\nawait moveFile(\"bestiary/aberration\", \"whistler\", \"whistler-jttrc\");\nawait moveFile(\"bestiary/aberration\", \"woe-strider\", \"woe-strider-mot\");\nawait moveFile(\"bestiary/aberration\", \"zodar\", \"zodar-bam\");\nawait moveFile(\"bestiary/beast\", \"almiraj\", \"almiraj-toa\");\nawait moveFile(\"bestiary/beast\", \"ammit\", \"ammit-psa\");\nawait moveFile(\"bestiary/beast\", \"amphisbaena\", \"amphisbaena-tftyp\");\nawait moveFile(\"bestiary/beast\", \"armored-saber-toothed-tiger\", \"armored-saber-toothed-tiger-cos\");\nawait moveFile(\"bestiary/beast\", \"aurochs\", \"aurochs-mpmm\");\nawait moveFile(\"bestiary/beast\", \"awakened-brown-bear\", \"awakened-brown-bear-wdmm\");\nawait moveFile(\"bestiary/beast\", \"awakened-elk\", \"awakened-elk-wdmm\");\nawait moveFile(\"bestiary/beast\", \"awakened-giant-wasp\", \"awakened-giant-wasp-wdmm\");\nawait moveFile(\"bestiary/beast\", \"awakened-rat\", \"awakened-rat-wdh\");\nawait moveFile(\"bestiary/beast\", \"awakened-white-moose\", \"awakened-white-moose-idrotf\");\nawait moveFile(\"bestiary/beast\", \"baloth\", \"baloth-psz\");\nawait moveFile(\"bestiary/beast\", \"beast-of-the-air\", \"beast-of-the-air-uaclassfeaturevariants\");\nawait moveFile(\"bestiary/beast\", \"beast-of-the-earth\", \"beast-of-the-earth-uaclassfeaturevariants\");\nawait moveFile(\"bestiary/beast\", \"beast-of-the-land\", \"beast-of-the-land-tce\");\nawait moveFile(\"bestiary/beast\", \"beast-of-the-sea\", \"beast-of-the-sea-tce\");\nawait moveFile(\"bestiary/beast\", \"beast-of-the-sky\", \"beast-of-the-sky-tce\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-2nd-level-spell\", \"bestial-spirit-2nd-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-3rd-level-spell\", \"bestial-spirit-3rd-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-4th-level-spell\", \"bestial-spirit-4th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-5th-level-spell\", \"bestial-spirit-5th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-6th-level-spell\", \"bestial-spirit-6th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-7th-level-spell\", \"bestial-spirit-7th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-8th-level-spell\", \"bestial-spirit-8th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-9th-level-spell\", \"bestial-spirit-9th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-air-2nd-level-spell\", \"bestial-spirit-air-2nd-level-spell-tce\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-air-3rd-level-spell\", \"bestial-spirit-air-3rd-level-spell-tce\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-air-4th-level-spell\", \"bestial-spirit-air-4th-level-spell-tce\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-air-5th-level-spell\", \"bestial-spirit-air-5th-level-spell-tce\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-air-6th-level-spell\", \"bestial-spirit-air-6th-level-spell-tce\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-air-7th-level-spell\", \"bestial-spirit-air-7th-level-spell-tce\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-air-8th-level-spell\", \"bestial-spirit-air-8th-level-spell-tce\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-air-9th-level-spell\", \"bestial-spirit-air-9th-level-spell-tce\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-land-2nd-level-spell\", \"bestial-spirit-land-2nd-level-spell-tce\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-land-3rd-level-spell\", \"bestial-spirit-land-3rd-level-spell-tce\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-land-4th-level-spell\", \"bestial-spirit-land-4th-level-spell-tce\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-land-5th-level-spell\", \"bestial-spirit-land-5th-level-spell-tce\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-land-6th-level-spell\", \"bestial-spirit-land-6th-level-spell-tce\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-land-7th-level-spell\", \"bestial-spirit-land-7th-level-spell-tce\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-land-8th-level-spell\", \"bestial-spirit-land-8th-level-spell-tce\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-land-9th-level-spell\", \"bestial-spirit-land-9th-level-spell-tce\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-water-2nd-level-spell\", \"bestial-spirit-water-2nd-level-spell-tce\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-water-3rd-level-spell\", \"bestial-spirit-water-3rd-level-spell-tce\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-water-4th-level-spell\", \"bestial-spirit-water-4th-level-spell-tce\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-water-5th-level-spell\", \"bestial-spirit-water-5th-level-spell-tce\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-water-6th-level-spell\", \"bestial-spirit-water-6th-level-spell-tce\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-water-7th-level-spell\", \"bestial-spirit-water-7th-level-spell-tce\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-water-8th-level-spell\", \"bestial-spirit-water-8th-level-spell-tce\");\nawait moveFile(\"bestiary/beast\", \"bestial-spirit-water-9th-level-spell\", \"bestial-spirit-water-9th-level-spell-tce\");\nawait moveFile(\"bestiary/beast\", \"bhaal-ravager\", \"bhaal-ravager-mabjov\");\nawait moveFile(\"bestiary/beast\", \"boontu-monkey\", \"boontu-monkey-awm\");\nawait moveFile(\"bestiary/beast\", \"bristled-moorbounder\", \"bristled-moorbounder-egw\");\nawait moveFile(\"bestiary/beast\", \"brontosaurus\", \"brontosaurus-mpmm\");\nawait moveFile(\"bestiary/beast\", \"cave-badger\", \"cave-badger-oota\");\nawait moveFile(\"bestiary/beast\", \"cerodon\", \"cerodon-psa\");\nawait moveFile(\"bestiary/beast\", \"chimeric-baboon\", \"chimeric-baboon-idrotf\");\nawait moveFile(\"bestiary/beast\", \"chimeric-cat\", \"chimeric-cat-idrotf\");\nawait moveFile(\"bestiary/beast\", \"chimeric-fox\", \"chimeric-fox-nrh-avitw\");\nawait moveFile(\"bestiary/beast\", \"chimeric-hare\", \"chimeric-hare-idrotf\");\nawait moveFile(\"bestiary/beast\", \"chimeric-rat\", \"chimeric-rat-idrotf\");\nawait moveFile(\"bestiary/beast\", \"chimeric-weasel\", \"chimeric-weasel-idrotf\");\nawait moveFile(\"bestiary/beast\", \"clawfoot\", \"clawfoot-erlw\");\nawait moveFile(\"bestiary/beast\", \"cow\", \"cow-vgm\");\nawait moveFile(\"bestiary/beast\", \"crow\", \"crow-wdmm\");\nawait moveFile(\"bestiary/beast\", \"deep-rothe\", \"deep-rothe-mpmm\");\nawait moveFile(\"bestiary/beast\", \"deep-spider\", \"deep-spider-mabjov\");\nawait moveFile(\"bestiary/beast\", \"deinonychus\", \"deinonychus-mpmm\");\nawait moveFile(\"bestiary/beast\", \"diatryma\", \"diatryma-wdh\");\nawait moveFile(\"bestiary/beast\", \"dimetrodon\", \"dimetrodon-mpmm\");\nawait moveFile(\"bestiary/beast\", \"dolphin\", \"dolphin-mpmm\");\nawait moveFile(\"bestiary/beast\", \"drake-large\", \"drake-large-psz\");\nawait moveFile(\"bestiary/beast\", \"drake-small\", \"drake-small-psz\");\nawait moveFile(\"bestiary/beast\", \"elder-giant-lizard\", \"elder-giant-lizard-tftyp\");\nawait moveFile(\"bestiary/beast\", \"enormous-tentacle\", \"enormous-tentacle-oow\");\nawait moveFile(\"bestiary/beast\", \"falcon\", \"falcon-wdh\");\nawait moveFile(\"bestiary/beast\", \"fastieth\", \"fastieth-erlw\");\nawait moveFile(\"bestiary/beast\", \"fiendish-giant-spider\", \"fiendish-giant-spider-oota\");\nawait moveFile(\"bestiary/beast\", \"fish\", \"fish-gos\");\nawait moveFile(\"bestiary/beast\", \"flying-monkey\", \"flying-monkey-toa\");\nawait moveFile(\"bestiary/beast\", \"fox\", \"fox-idrotf\");\nawait moveFile(\"bestiary/beast\", \"frilled-deathspitter\", \"frilled-deathspitter-psx\");\nawait moveFile(\"bestiary/beast\", \"giant-canary\", \"giant-canary-ftd\");\nawait moveFile(\"bestiary/beast\", \"giant-coral-snake\", \"giant-coral-snake-gos\");\nawait moveFile(\"bestiary/beast\", \"giant-crayfish\", \"giant-crayfish-tftyp\");\nawait moveFile(\"bestiary/beast\", \"giant-dragonfly\", \"giant-dragonfly-wbtw\");\nawait moveFile(\"bestiary/beast\", \"giant-flying-spider\", \"giant-flying-spider-wdmm\");\nawait moveFile(\"bestiary/beast\", \"giant-lightning-eel\", \"giant-lightning-eel-tftyp\");\nawait moveFile(\"bestiary/beast\", \"giant-raven\", \"giant-raven-skt\");\nawait moveFile(\"bestiary/beast\", \"giant-riding-lizard\", \"giant-riding-lizard-oota\");\nawait moveFile(\"bestiary/beast\", \"giant-rocktopus\", \"giant-rocktopus-oota\");\nawait moveFile(\"bestiary/beast\", \"giant-sea-eel\", \"giant-sea-eel-gos\");\nawait moveFile(\"bestiary/beast\", \"giant-snail\", \"giant-snail-wbtw\");\nawait moveFile(\"bestiary/beast\", \"giant-snapping-turtle\", \"giant-snapping-turtle-toa\");\nawait moveFile(\"bestiary/beast\", \"giant-space-hamster\", \"giant-space-hamster-bam\");\nawait moveFile(\"bestiary/beast\", \"giant-subterranean-lizard\", \"giant-subterranean-lizard-tftyp\");\nawait moveFile(\"bestiary/beast\", \"giant-swan\", \"giant-swan-wbtw\");\nawait moveFile(\"bestiary/beast\", \"giant-walrus\", \"giant-walrus-idrotf\");\nawait moveFile(\"bestiary/beast\", \"giant-white-moray-eel\", \"giant-white-moray-eel-gos\");\nawait moveFile(\"bestiary/beast\", \"gnarlid\", \"gnarlid-psz\");\nawait moveFile(\"bestiary/beast\", \"golden-stag\", \"golden-stag-hotdq\");\nawait moveFile(\"bestiary/beast\", \"great-cat\", \"great-cat-psz\");\nawait moveFile(\"bestiary/beast\", \"gremlin\", \"gremlin-psk\");\nawait moveFile(\"bestiary/beast\", \"hadrosaurus\", \"hadrosaurus-mpmm\");\nawait moveFile(\"bestiary/beast\", \"hare\", \"hare-idrotf\");\nawait moveFile(\"bestiary/beast\", \"heartstabber-mosquito\", \"heartstabber-mosquito-psz\");\nawait moveFile(\"bestiary/beast\", \"hellwasp-grub\", \"hellwasp-grub-bgdia\");\nawait moveFile(\"bestiary/beast\", \"hippopotamus\", \"hippopotamus-psa\");\nawait moveFile(\"bestiary/beast\", \"huge-giant-crab\", \"huge-giant-crab-tftyp\");\nawait moveFile(\"bestiary/beast\", \"huge-polar-bear\", \"huge-polar-bear-tftyp\");\nawait moveFile(\"bestiary/beast\", \"hulking-crab\", \"hulking-crab-skt\");\nawait moveFile(\"bestiary/beast\", \"ice-spider-queen\", \"ice-spider-queen-skt\");\nawait moveFile(\"bestiary/beast\", \"ice-spider\", \"ice-spider-skt\");\nawait moveFile(\"bestiary/beast\", \"jaculi\", \"jaculi-toa\");\nawait moveFile(\"bestiary/beast\", \"kavu-predator\", \"kavu-predator-psd\");\nawait moveFile(\"bestiary/beast\", \"knucklehead-trout\", \"knucklehead-trout-idrotf\");\nawait moveFile(\"bestiary/beast\", \"koi-prawn\", \"koi-prawn-jttrc\");\nawait moveFile(\"bestiary/beast\", \"large-drake\", \"large-drake-psa\");\nawait moveFile(\"bestiary/beast\", \"moonshark\", \"moonshark-crcotn\");\nawait moveFile(\"bestiary/beast\", \"moorbounder\", \"moorbounder-egw\");\nawait moveFile(\"bestiary/beast\", \"mountain-goat\", \"mountain-goat-idrotf\");\nawait moveFile(\"bestiary/beast\", \"ox\", \"ox-mpmm\");\nawait moveFile(\"bestiary/beast\", \"oxen\", \"oxen-psz\");\nawait moveFile(\"bestiary/beast\", \"peacock\", \"peacock-bgdia\");\nawait moveFile(\"bestiary/beast\", \"pig\", \"pig-skt\");\nawait moveFile(\"bestiary/beast\", \"primeval-companion\", \"primeval-companion-ua2022giantoptions\");\nawait moveFile(\"bestiary/beast\", \"quetzalcoatlus\", \"quetzalcoatlus-mpmm\");\nawait moveFile(\"bestiary/beast\", \"reindeer\", \"reindeer-idrotf\");\nawait moveFile(\"bestiary/beast\", \"relic-sloth\", \"relic-sloth-scc\");\nawait moveFile(\"bestiary/beast\", \"river-serpent\", \"river-serpent-psa\");\nawait moveFile(\"bestiary/beast\", \"rooster\", \"rooster-jttrc\");\nawait moveFile(\"bestiary/beast\", \"rothe\", \"rothe-vgm\");\nawait moveFile(\"bestiary/beast\", \"sahuagin-hatchling-swarm\", \"sahuagin-hatchling-swarm-gos\");\nawait moveFile(\"bestiary/beast\", \"seal\", \"seal-idrotf\");\nawait moveFile(\"bestiary/beast\", \"serpopard\", \"serpopard-psa\");\nawait moveFile(\"bestiary/beast\", \"sheep\", \"sheep-skt\");\nawait moveFile(\"bestiary/beast\", \"shoal-serpent\", \"shoal-serpent-psz\");\nawait moveFile(\"bestiary/beast\", \"sky-leviathan\", \"sky-leviathan-psk\");\nawait moveFile(\"bestiary/beast\", \"sled-dog\", \"sled-dog-rot\");\nawait moveFile(\"bestiary/beast\", \"small-drake\", \"small-drake-psa\");\nawait moveFile(\"bestiary/beast\", \"snow-leopard\", \"snow-leopard-tftyp\");\nawait moveFile(\"bestiary/beast\", \"space-eel\", \"space-eel-bam\");\nawait moveFile(\"bestiary/beast\", \"space-guppy\", \"space-guppy-bam\");\nawait moveFile(\"bestiary/beast\", \"space-hamster\", \"space-hamster-wdmm\");\nawait moveFile(\"bestiary/beast\", \"space-mollymawk\", \"space-mollymawk-bam\");\nawait moveFile(\"bestiary/beast\", \"space-swine\", \"space-swine-bam\");\nawait moveFile(\"bestiary/beast\", \"sperm-whale\", \"sperm-whale-idrotf\");\nawait moveFile(\"bestiary/beast\", \"spiderfrog\", \"spiderfrog-mgelft\");\nawait moveFile(\"bestiary/beast\", \"steel-leaf-kavu\", \"steel-leaf-kavu-psd\");\nawait moveFile(\"bestiary/beast\", \"stegosaurus\", \"stegosaurus-mpmm\");\nawait moveFile(\"bestiary/beast\", \"swarm-of-maggots\", \"swarm-of-maggots-vrgr\");\nawait moveFile(\"bestiary/beast\", \"swarm-of-rot-grubs\", \"swarm-of-rot-grubs-mpmm\");\nawait moveFile(\"bestiary/beast\", \"swarm-of-scarabs\", \"swarm-of-scarabs-vrgr\");\nawait moveFile(\"bestiary/beast\", \"sword-spider\", \"sword-spider-mabjov\");\nawait moveFile(\"bestiary/beast\", \"terastodon\", \"terastodon-psz\");\nawait moveFile(\"bestiary/beast\", \"terra-stomper\", \"terra-stomper-psz\");\nawait moveFile(\"bestiary/beast\", \"two-headed-crocodile\", \"two-headed-crocodile-imr\");\nawait moveFile(\"bestiary/beast\", \"two-headed-plesiosaurus\", \"two-headed-plesiosaurus-ttp\");\nawait moveFile(\"bestiary/beast\", \"velociraptor\", \"velociraptor-mpmm\");\nawait moveFile(\"bestiary/beast\", \"walrus\", \"walrus-idrotf\");\nawait moveFile(\"bestiary/beast\", \"wild-dog\", \"wild-dog-toa\");\nawait moveFile(\"bestiary/beast\", \"woodcrasher-baloth\", \"woodcrasher-baloth-psz\");\nawait moveFile(\"bestiary/beast\", \"yak\", \"yak-skt\");\nawait moveFile(\"bestiary/beast\", \"young-bulette\", \"young-bulette-pota\");\nawait moveFile(\"bestiary/beast\", \"young-horizonback-tortoise\", \"young-horizonback-tortoise-crcotn\");\nawait moveFile(\"bestiary/beast\", \"young-winter-wolf\", \"young-winter-wolf-tftyp\");\nawait moveFile(\"bestiary/beast\", \"zebra\", \"zebra-toa\");\nawait moveFile(\"bestiary/celestial\", \"angel-of-amonkhet\", \"angel-of-amonkhet-psa\");\nawait moveFile(\"bestiary/celestial\", \"angel\", \"angel-psz\");\nawait moveFile(\"bestiary/celestial\", \"archaic\", \"archaic-scc\");\nawait moveFile(\"bestiary/celestial\", \"archon-of-falling-stars\", \"archon-of-falling-stars-mot\");\nawait moveFile(\"bestiary/celestial\", \"archon-of-redemption\", \"archon-of-redemption-psz\");\nawait moveFile(\"bestiary/celestial\", \"archon-of-the-triumvirate\", \"archon-of-the-triumvirate-ggr\");\nawait moveFile(\"bestiary/celestial\", \"ashen-rider\", \"ashen-rider-mot\");\nawait moveFile(\"bestiary/celestial\", \"battleforce-angel\", \"battleforce-angel-ggr\");\nawait moveFile(\"bestiary/celestial\", \"celestial-spirit-5th-level-spell\", \"celestial-spirit-5th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/celestial\", \"celestial-spirit-6th-level-spell\", \"celestial-spirit-6th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/celestial\", \"celestial-spirit-7th-level-spell\", \"celestial-spirit-7th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/celestial\", \"celestial-spirit-8th-level-spell\", \"celestial-spirit-8th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/celestial\", \"celestial-spirit-9th-level-spell\", \"celestial-spirit-9th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/celestial\", \"deathpact-angel\", \"deathpact-angel-ggr\");\nawait moveFile(\"bestiary/celestial\", \"felidar\", \"felidar-ggr\");\nawait moveFile(\"bestiary/celestial\", \"firemane-angel\", \"firemane-angel-ggr\");\nawait moveFile(\"bestiary/celestial\", \"flight-alabaster-angel\", \"flight-alabaster-angel-psi\");\nawait moveFile(\"bestiary/celestial\", \"flight-goldnight-angel\", \"flight-goldnight-angel-psi\");\nawait moveFile(\"bestiary/celestial\", \"flight-of-moonsilver-angel\", \"flight-of-moonsilver-angel-psi\");\nawait moveFile(\"bestiary/celestial\", \"hollyphant\", \"hollyphant-bgdia\");\nawait moveFile(\"bestiary/celestial\", \"host-of-herons-angel\", \"host-of-herons-angel-psi\");\nawait moveFile(\"bestiary/celestial\", \"ki-rin\", \"ki-rin-mpmm\");\nawait moveFile(\"bestiary/celestial\", \"kindori\", \"kindori-bam\");\nawait moveFile(\"bestiary/celestial\", \"pari\", \"pari-jttrc\");\nawait moveFile(\"bestiary/celestial\", \"radiant-idol\", \"radiant-idol-erlw\");\nawait moveFile(\"bestiary/celestial\", \"reigar\", \"reigar-bam\");\nawait moveFile(\"bestiary/celestial\", \"star-lancer\", \"star-lancer-mcv1sc\");\nawait moveFile(\"bestiary/celestial\", \"starlight-apparition\", \"starlight-apparition-bam\");\nawait moveFile(\"bestiary/celestial\", \"winged-bull\", \"winged-bull-mot\");\nawait moveFile(\"bestiary/celestial\", \"winged-lion\", \"winged-lion-mot\");\nawait moveFile(\"bestiary/construct\", \"alchemical-homunculus\", \"alchemical-homunculus-uaartificerrevisited\");\nawait moveFile(\"bestiary/construct\", \"alyxian-the-hunter\", \"alyxian-the-hunter-crcotn\");\nawait moveFile(\"bestiary/construct\", \"amber-golem\", \"amber-golem-cos\");\nawait moveFile(\"bestiary/construct\", \"ancient-companion\", \"ancient-companion-ua2021magesofstrixhaven\");\nawait moveFile(\"bestiary/construct\", \"animated-ballista\", \"animated-ballista-wdmm\");\nawait moveFile(\"bestiary/construct\", \"animated-broom\", \"animated-broom-cm\");\nawait moveFile(\"bestiary/construct\", \"animated-chained-library\", \"animated-chained-library-cm\");\nawait moveFile(\"bestiary/construct\", \"animated-glass-statue\", \"animated-glass-statue-crcotn\");\nawait moveFile(\"bestiary/construct\", \"animated-halberd\", \"animated-halberd-cos\");\nawait moveFile(\"bestiary/construct\", \"animated-jade-serpent\", \"animated-jade-serpent-wdmm\");\nawait moveFile(\"bestiary/construct\", \"animated-knife\", \"animated-knife-egw\");\nawait moveFile(\"bestiary/construct\", \"animated-staff\", \"animated-staff-wdmm\");\nawait moveFile(\"bestiary/construct\", \"animated-statue-of-lolth\", \"animated-statue-of-lolth-wdmm\");\nawait moveFile(\"bestiary/construct\", \"animated-stove\", \"animated-stove-wdmm\");\nawait moveFile(\"bestiary/construct\", \"animated-table\", \"animated-table-tftyp\");\nawait moveFile(\"bestiary/construct\", \"animated-wand\", \"animated-wand-wdmm\");\nawait moveFile(\"bestiary/construct\", \"anvilwrought-raptor\", \"anvilwrought-raptor-mot\");\nawait moveFile(\"bestiary/construct\", \"autognome\", \"autognome-bam\");\nawait moveFile(\"bestiary/construct\", \"bore-worm\", \"bore-worm-wdmm\");\nawait moveFile(\"bestiary/construct\", \"brain-in-iron\", \"brain-in-iron-imr\");\nawait moveFile(\"bestiary/construct\", \"bronze-sable\", \"bronze-sable-mot\");\nawait moveFile(\"bestiary/construct\", \"broom-of-animated-attack\", \"broom-of-animated-attack-cos\");\nawait moveFile(\"bestiary/construct\", \"burnished-hart\", \"burnished-hart-mot\");\nawait moveFile(\"bestiary/construct\", \"cadaver-collector\", \"cadaver-collector-mpmm\");\nawait moveFile(\"bestiary/construct\", \"canopic-golem\", \"canopic-golem-cm\");\nawait moveFile(\"bestiary/construct\", \"carrionette\", \"carrionette-vrgr\");\nawait moveFile(\"bestiary/construct\", \"chardalyn-dragon\", \"chardalyn-dragon-idrotf\");\nawait moveFile(\"bestiary/construct\", \"clay-gladiator\", \"clay-gladiator-toa\");\nawait moveFile(\"bestiary/construct\", \"clockwork-behir\", \"clockwork-behir-oow\");\nawait moveFile(\"bestiary/construct\", \"clockwork-bronze-scout\", \"clockwork-bronze-scout-mpmm\");\nawait moveFile(\"bestiary/construct\", \"clockwork-dragon\", \"clockwork-dragon-ai\");\nawait moveFile(\"bestiary/construct\", \"clockwork-horror\", \"clockwork-horror-mcv1sc\");\nawait moveFile(\"bestiary/construct\", \"clockwork-iron-cobra\", \"clockwork-iron-cobra-mpmm\");\nawait moveFile(\"bestiary/construct\", \"clockwork-kraken\", \"clockwork-kraken-llk\");\nawait moveFile(\"bestiary/construct\", \"clockwork-mule\", \"clockwork-mule-skt\");\nawait moveFile(\"bestiary/construct\", \"clockwork-oaken-bolter\", \"clockwork-oaken-bolter-mpmm\");\nawait moveFile(\"bestiary/construct\", \"clockwork-stone-defender\", \"clockwork-stone-defender-mpmm\");\nawait moveFile(\"bestiary/construct\", \"cogwork-archivist\", \"cogwork-archivist-scc\");\nawait moveFile(\"bestiary/construct\", \"colossus-of-akros\", \"colossus-of-akros-mot\");\nawait moveFile(\"bestiary/construct\", \"construct-spirit-4th-level-spell\", \"construct-spirit-4th-level-spell-tce\");\nawait moveFile(\"bestiary/construct\", \"construct-spirit-5th-level-spell\", \"construct-spirit-5th-level-spell-tce\");\nawait moveFile(\"bestiary/construct\", \"construct-spirit-6th-level-spell\", \"construct-spirit-6th-level-spell-tce\");\nawait moveFile(\"bestiary/construct\", \"construct-spirit-7th-level-spell\", \"construct-spirit-7th-level-spell-tce\");\nawait moveFile(\"bestiary/construct\", \"construct-spirit-8th-level-spell\", \"construct-spirit-8th-level-spell-tce\");\nawait moveFile(\"bestiary/construct\", \"construct-spirit-9th-level-spell\", \"construct-spirit-9th-level-spell-tce\");\nawait moveFile(\"bestiary/construct\", \"constructed-commoner\", \"constructed-commoner-cm\");\nawait moveFile(\"bestiary/construct\", \"creepy-doll\", \"creepy-doll-psi\");\nawait moveFile(\"bestiary/construct\", \"crystal-battleaxe\", \"crystal-battleaxe-wdmm\");\nawait moveFile(\"bestiary/construct\", \"crystal-golem\", \"crystal-golem-wdmm\");\nawait moveFile(\"bestiary/construct\", \"damaged-flesh-golem\", \"damaged-flesh-golem-egw\");\nawait moveFile(\"bestiary/construct\", \"dancing-item\", \"dancing-item-ua2020subclassespt2\");\nawait moveFile(\"bestiary/construct\", \"demos-magen\", \"demos-magen-idrotf\");\nawait moveFile(\"bestiary/construct\", \"dragonbone-golem\", \"dragonbone-golem-ftd\");\nawait moveFile(\"bestiary/construct\", \"duergar-hammerer\", \"duergar-hammerer-mpmm\");\nawait moveFile(\"bestiary/construct\", \"duergar-screamer\", \"duergar-screamer-mpmm\");\nawait moveFile(\"bestiary/construct\", \"expeditious-messenger\", \"expeditious-messenger-erlw\");\nawait moveFile(\"bestiary/construct\", \"fiendish-flesh-golem\", \"fiendish-flesh-golem-bgdia\");\nawait moveFile(\"bestiary/construct\", \"flying-dagger\", \"flying-dagger-bgdia\");\nawait moveFile(\"bestiary/construct\", \"flying-rocking-horse\", \"flying-rocking-horse-wbtw\");\nawait moveFile(\"bestiary/construct\", \"flying-shield\", \"flying-shield-tftyp\");\nawait moveFile(\"bestiary/construct\", \"flying-staff\", \"flying-staff-wdh\");\nawait moveFile(\"bestiary/construct\", \"flying-trident\", \"flying-trident-wdmm\");\nawait moveFile(\"bestiary/construct\", \"four-armed-statue\", \"four-armed-statue-oota\");\nawait moveFile(\"bestiary/construct\", \"fractal-mascot\", \"fractal-mascot-scc\");\nawait moveFile(\"bestiary/construct\", \"fractine\", \"fractine-mcv1sc\");\nawait moveFile(\"bestiary/construct\", \"galvan-magen\", \"galvan-magen-idrotf\");\nawait moveFile(\"bestiary/construct\", \"gargantuan-rug-of-smothering\", \"gargantuan-rug-of-smothering-tftyp\");\nawait moveFile(\"bestiary/construct\", \"gearkeeper-construct\", \"gearkeeper-construct-egw\");\nawait moveFile(\"bestiary/construct\", \"glass-pegasus\", \"glass-pegasus-wbtw\");\nawait moveFile(\"bestiary/construct\", \"glasswork-golem\", \"glasswork-golem-wbtw\");\nawait moveFile(\"bestiary/construct\", \"gold-forged-sentinel\", \"gold-forged-sentinel-mot\");\nawait moveFile(\"bestiary/construct\", \"guardian-portrait\", \"guardian-portrait-cos\");\nawait moveFile(\"bestiary/construct\", \"halaster-horror\", \"halaster-horror-wdmm\");\nawait moveFile(\"bestiary/construct\", \"headless-iron-golem\", \"headless-iron-golem-idrotf\");\nawait moveFile(\"bestiary/construct\", \"hellfire-engine\", \"hellfire-engine-mpmm\");\nawait moveFile(\"bestiary/construct\", \"homunculus-servant\", \"homunculus-servant-tce\");\nawait moveFile(\"bestiary/construct\", \"huge-stone-golem\", \"huge-stone-golem-skt\");\nawait moveFile(\"bestiary/construct\", \"hypnos-magen\", \"hypnos-magen-idrotf\");\nawait moveFile(\"bestiary/construct\", \"iron-defender\", \"iron-defender-erlw\");\nawait moveFile(\"bestiary/construct\", \"jade-giant-spider\", \"jade-giant-spider-oota\");\nawait moveFile(\"bestiary/construct\", \"keg-robot\", \"keg-robot-ai\");\nawait moveFile(\"bestiary/construct\", \"kiddywidget\", \"kiddywidget-cm\");\nawait moveFile(\"bestiary/construct\", \"lightning-golem\", \"lightning-golem-cm\");\nawait moveFile(\"bestiary/construct\", \"living-bigbys-hand\", \"living-bigbys-hand-idrotf\");\nawait moveFile(\"bestiary/construct\", \"living-blade-of-disaster\", \"living-blade-of-disaster-idrotf\");\nawait moveFile(\"bestiary/construct\", \"living-burning-hands\", \"living-burning-hands-erlw\");\nawait moveFile(\"bestiary/construct\", \"living-cloudkill\", \"living-cloudkill-erlw\");\nawait moveFile(\"bestiary/construct\", \"living-demiplane\", \"living-demiplane-idrotf\");\nawait moveFile(\"bestiary/construct\", \"living-doll\", \"living-doll-wbtw\");\nawait moveFile(\"bestiary/construct\", \"living-iron-statue\", \"living-iron-statue-gos\");\nawait moveFile(\"bestiary/construct\", \"living-lightning-bolt\", \"living-lightning-bolt-erlw\");\nawait moveFile(\"bestiary/construct\", \"living-unseen-servant\", \"living-unseen-servant-wdmm\");\nawait moveFile(\"bestiary/construct\", \"mad-golem\", \"mad-golem-wdmm\");\nawait moveFile(\"bestiary/construct\", \"marut\", \"marut-mpmm\");\nawait moveFile(\"bestiary/construct\", \"mechachimera\", \"mechachimera-oow\");\nawait moveFile(\"bestiary/construct\", \"mechanical-bird\", \"mechanical-bird-wdh\");\nawait moveFile(\"bestiary/construct\", \"metal-wasp\", \"metal-wasp-wdmm\");\nawait moveFile(\"bestiary/construct\", \"metallic-peacekeeper\", \"metallic-peacekeeper-ftd\");\nawait moveFile(\"bestiary/construct\", \"metallic-warbler\", \"metallic-warbler-ftd\");\nawait moveFile(\"bestiary/construct\", \"mighty-servant-of-leuk-o\", \"mighty-servant-of-leuk-o-tce\");\nawait moveFile(\"bestiary/construct\", \"minotaur-living-crystal-statue\", \"minotaur-living-crystal-statue-gos\");\nawait moveFile(\"bestiary/construct\", \"nimblewright\", \"nimblewright-wdh\");\nawait moveFile(\"bestiary/construct\", \"paper-bird\", \"paper-bird-wbtw\");\nawait moveFile(\"bestiary/construct\", \"paper-whirlwind\", \"paper-whirlwind-rot\");\nawait moveFile(\"bestiary/construct\", \"reduced-threat-clay-golem\", \"reduced-threat-clay-golem-tftyp\");\nawait moveFile(\"bestiary/construct\", \"reduced-threat-flesh-golem\", \"reduced-threat-flesh-golem-tftyp\");\nawait moveFile(\"bestiary/construct\", \"reduced-threat-helmed-horror\", \"reduced-threat-helmed-horror-tftyp\");\nawait moveFile(\"bestiary/construct\", \"reduced-threat-stone-golem\", \"reduced-threat-stone-golem-tftyp\");\nawait moveFile(\"bestiary/construct\", \"replica-duodrone\", \"replica-duodrone-oow\");\nawait moveFile(\"bestiary/construct\", \"replica-monodrone\", \"replica-monodrone-oow\");\nawait moveFile(\"bestiary/construct\", \"replica-pentadrone\", \"replica-pentadrone-oow\");\nawait moveFile(\"bestiary/construct\", \"replica-quadrone\", \"replica-quadrone-oow\");\nawait moveFile(\"bestiary/construct\", \"replica-tridrone\", \"replica-tridrone-oow\");\nawait moveFile(\"bestiary/construct\", \"retriever\", \"retriever-mpmm\");\nawait moveFile(\"bestiary/construct\", \"rotter\", \"rotter-rtg\");\nawait moveFile(\"bestiary/construct\", \"ruidium-elephant\", \"ruidium-elephant-crcotn\");\nawait moveFile(\"bestiary/construct\", \"ruin-grinder\", \"ruin-grinder-scc\");\nawait moveFile(\"bestiary/construct\", \"sacred-statue\", \"sacred-statue-mpmm\");\nawait moveFile(\"bestiary/construct\", \"sapphire-sentinel\", \"sapphire-sentinel-cm\");\nawait moveFile(\"bestiary/construct\", \"scaladar\", \"scaladar-wdmm\");\nawait moveFile(\"bestiary/construct\", \"scarlet-sentinel\", \"scarlet-sentinel-nrh-avitw\");\nawait moveFile(\"bestiary/construct\", \"scufflecup-teacup\", \"scufflecup-teacup-scc\");\nawait moveFile(\"bestiary/construct\", \"servitor-thrull\", \"servitor-thrull-ggr\");\nawait moveFile(\"bestiary/construct\", \"servo\", \"servo-psk\");\nawait moveFile(\"bestiary/construct\", \"skaab\", \"skaab-psi\");\nawait moveFile(\"bestiary/construct\", \"skitterwidget\", \"skitterwidget-cm\");\nawait moveFile(\"bestiary/construct\", \"skull-flier\", \"skull-flier-slw\");\nawait moveFile(\"bestiary/construct\", \"snake-horror\", \"snake-horror-rot\");\nawait moveFile(\"bestiary/construct\", \"snow-golem\", \"snow-golem-idrotf\");\nawait moveFile(\"bestiary/construct\", \"spiked-tomb-guardian\", \"spiked-tomb-guardian-toa\");\nawait moveFile(\"bestiary/construct\", \"spirit-statue-mascot\", \"spirit-statue-mascot-scc\");\nawait moveFile(\"bestiary/construct\", \"statue-of-vergadain\", \"statue-of-vergadain-wdmm\");\nawait moveFile(\"bestiary/construct\", \"steel-defender\", \"steel-defender-tce\");\nawait moveFile(\"bestiary/construct\", \"steel-predator\", \"steel-predator-mpmm\");\nawait moveFile(\"bestiary/construct\", \"stone-cursed\", \"stone-cursed-mpmm\");\nawait moveFile(\"bestiary/construct\", \"stone-dragon-statue\", \"stone-dragon-statue-tftyp\");\nawait moveFile(\"bestiary/construct\", \"stone-giant-statue\", \"stone-giant-statue-skt\");\nawait moveFile(\"bestiary/construct\", \"stone-juggernaut\", \"stone-juggernaut-toa\");\nawait moveFile(\"bestiary/construct\", \"stone-warrior\", \"stone-warrior-pota\");\nawait moveFile(\"bestiary/construct\", \"stonecloak\", \"stonecloak-wdmm\");\nawait moveFile(\"bestiary/construct\", \"strixhaven-campus-guide\", \"strixhaven-campus-guide-scc\");\nawait moveFile(\"bestiary/construct\", \"swarm-of-animated-books\", \"swarm-of-animated-books-cm\");\nawait moveFile(\"bestiary/construct\", \"swarm-of-books\", \"swarm-of-books-wdh\");\nawait moveFile(\"bestiary/construct\", \"swarm-of-mechanical-spiders\", \"swarm-of-mechanical-spiders-wdh\");\nawait moveFile(\"bestiary/construct\", \"terracotta-warrior\", \"terracotta-warrior-toa\");\nawait moveFile(\"bestiary/construct\", \"thessalheart-construct\", \"thessalheart-construct-imr\");\nawait moveFile(\"bestiary/construct\", \"tin-soldier\", \"tin-soldier-wbtw\");\nawait moveFile(\"bestiary/construct\", \"tiny-servant\", \"tiny-servant-xge\");\nawait moveFile(\"bestiary/construct\", \"tomb-guardian\", \"tomb-guardian-toa\");\nawait moveFile(\"bestiary/construct\", \"tomb-tapper\", \"tomb-tapper-idrotf\");\nawait moveFile(\"bestiary/construct\", \"vampiric-jade-statue\", \"vampiric-jade-statue-gos\");\nawait moveFile(\"bestiary/construct\", \"vox-seeker\", \"vox-seeker-egw\");\nawait moveFile(\"bestiary/construct\", \"walking-statue-of-waterdeep\", \"walking-statue-of-waterdeep-wdh\");\nawait moveFile(\"bestiary/construct\", \"warforged-colossus\", \"warforged-colossus-erlw\");\nawait moveFile(\"bestiary/construct\", \"warforged-titan\", \"warforged-titan-erlw\");\nawait moveFile(\"bestiary/construct\", \"winged-thrull\", \"winged-thrull-ggr\");\nawait moveFile(\"bestiary/construct\", \"wooden-donkey\", \"wooden-donkey-wdmm\");\nawait moveFile(\"bestiary/construct\", \"zendikar-golem\", \"zendikar-golem-psz\");\nawait moveFile(\"bestiary/dragon\", \"adult-amethyst-dragon\", \"adult-amethyst-dragon-ftd\");\nawait moveFile(\"bestiary/dragon\", \"adult-amonkhet-dragon\", \"adult-amonkhet-dragon-psa\");\nawait moveFile(\"bestiary/dragon\", \"adult-crystal-dragon\", \"adult-crystal-dragon-ftd\");\nawait moveFile(\"bestiary/dragon\", \"adult-deep-dragon\", \"adult-deep-dragon-ftd\");\nawait moveFile(\"bestiary/dragon\", \"adult-emerald-dragon\", \"adult-emerald-dragon-ftd\");\nawait moveFile(\"bestiary/dragon\", \"adult-lunar-dragon\", \"adult-lunar-dragon-bam\");\nawait moveFile(\"bestiary/dragon\", \"adult-moonstone-dragon\", \"adult-moonstone-dragon-ftd\");\nawait moveFile(\"bestiary/dragon\", \"adult-sapphire-dragon\", \"adult-sapphire-dragon-ftd\");\nawait moveFile(\"bestiary/dragon\", \"adult-solar-dragon\", \"adult-solar-dragon-bam\");\nawait moveFile(\"bestiary/dragon\", \"adult-topaz-dragon\", \"adult-topaz-dragon-ftd\");\nawait moveFile(\"bestiary/dragon\", \"ambush-drake\", \"ambush-drake-hotdq\");\nawait moveFile(\"bestiary/dragon\", \"amethyst-dragon-wyrmling\", \"amethyst-dragon-wyrmling-ftd\");\nawait moveFile(\"bestiary/dragon\", \"amethyst-greatwyrm\", \"amethyst-greatwyrm-ftd\");\nawait moveFile(\"bestiary/dragon\", \"amonkhet-dragon-wyrmling\", \"amonkhet-dragon-wyrmling-psa\");\nawait moveFile(\"bestiary/dragon\", \"ancient-amethyst-dragon\", \"ancient-amethyst-dragon-ftd\");\nawait moveFile(\"bestiary/dragon\", \"ancient-amonkhet-dragon\", \"ancient-amonkhet-dragon-psa\");\nawait moveFile(\"bestiary/dragon\", \"ancient-crystal-dragon\", \"ancient-crystal-dragon-ftd\");\nawait moveFile(\"bestiary/dragon\", \"ancient-deep-dragon\", \"ancient-deep-dragon-ftd\");\nawait moveFile(\"bestiary/dragon\", \"ancient-dragon-turtle\", \"ancient-dragon-turtle-ftd\");\nawait moveFile(\"bestiary/dragon\", \"ancient-emerald-dragon\", \"ancient-emerald-dragon-ftd\");\nawait moveFile(\"bestiary/dragon\", \"ancient-lunar-dragon\", \"ancient-lunar-dragon-bam\");\nawait moveFile(\"bestiary/dragon\", \"ancient-moonstone-dragon\", \"ancient-moonstone-dragon-ftd\");\nawait moveFile(\"bestiary/dragon\", \"ancient-sapphire-dragon\", \"ancient-sapphire-dragon-ftd\");\nawait moveFile(\"bestiary/dragon\", \"ancient-sea-serpent\", \"ancient-sea-serpent-ftd\");\nawait moveFile(\"bestiary/dragon\", \"ancient-solar-dragon\", \"ancient-solar-dragon-bam\");\nawait moveFile(\"bestiary/dragon\", \"ancient-topaz-dragon\", \"ancient-topaz-dragon-ftd\");\nawait moveFile(\"bestiary/dragon\", \"aspect-of-bahamut\", \"aspect-of-bahamut-ftd\");\nawait moveFile(\"bestiary/dragon\", \"aspect-of-tiamat\", \"aspect-of-tiamat-ftd\");\nawait moveFile(\"bestiary/dragon\", \"bakunawa\", \"bakunawa-jttrc\");\nawait moveFile(\"bestiary/dragon\", \"black-greatwyrm\", \"black-greatwyrm-ftd\");\nawait moveFile(\"bestiary/dragon\", \"blue-greatwyrm\", \"blue-greatwyrm-ftd\");\nawait moveFile(\"bestiary/dragon\", \"brass-greatwyrm\", \"brass-greatwyrm-ftd\");\nawait moveFile(\"bestiary/dragon\", \"bronze-greatwyrm\", \"bronze-greatwyrm-ftd\");\nawait moveFile(\"bestiary/dragon\", \"copper-greatwyrm\", \"copper-greatwyrm-ftd\");\nawait moveFile(\"bestiary/dragon\", \"crystal-dragon-wyrmling\", \"crystal-dragon-wyrmling-ftd\");\nawait moveFile(\"bestiary/dragon\", \"crystal-greatwyrm\", \"crystal-greatwyrm-ftd\");\nawait moveFile(\"bestiary/dragon\", \"deep-dragon-wyrmling\", \"deep-dragon-wyrmling-ftd\");\nawait moveFile(\"bestiary/dragon\", \"draconic-spirit-5th-level-spell\", \"draconic-spirit-5th-level-spell-ftd\");\nawait moveFile(\"bestiary/dragon\", \"draconic-spirit-6th-level-spell\", \"draconic-spirit-6th-level-spell-ua2021draconicoptions\");\nawait moveFile(\"bestiary/dragon\", \"draconic-spirit-7th-level-spell\", \"draconic-spirit-7th-level-spell-ftd\");\nawait moveFile(\"bestiary/dragon\", \"draconic-spirit-8th-level-spell\", \"draconic-spirit-8th-level-spell-ua2021draconicoptions\");\nawait moveFile(\"bestiary/dragon\", \"draconic-spirit-9th-level-spell\", \"draconic-spirit-9th-level-spell-ftd\");\nawait moveFile(\"bestiary/dragon\", \"dragon-tortoise\", \"dragon-tortoise-cm\");\nawait moveFile(\"bestiary/dragon\", \"dragon-turtle-wyrmling\", \"dragon-turtle-wyrmling-ftd\");\nawait moveFile(\"bestiary/dragon\", \"dragonnel\", \"dragonnel-ftd\");\nawait moveFile(\"bestiary/dragon\", \"drake-companion\", \"drake-companion-ftd\");\nawait moveFile(\"bestiary/dragon\", \"emerald-dragon-wyrmling\", \"emerald-dragon-wyrmling-ftd\");\nawait moveFile(\"bestiary/dragon\", \"emerald-greatwyrm\", \"emerald-greatwyrm-ftd\");\nawait moveFile(\"bestiary/dragon\", \"gold-greatwyrm\", \"gold-greatwyrm-ftd\");\nawait moveFile(\"bestiary/dragon\", \"green-greatwyrm\", \"green-greatwyrm-ftd\");\nawait moveFile(\"bestiary/dragon\", \"guard-drake\", \"guard-drake-mpmm\");\nawait moveFile(\"bestiary/dragon\", \"jabberwock\", \"jabberwock-wbtw\");\nawait moveFile(\"bestiary/dragon\", \"kobold-dragonshield\", \"kobold-dragonshield-mpmm\");\nawait moveFile(\"bestiary/dragon\", \"lunar-dragon-wyrmling\", \"lunar-dragon-wyrmling-bam\");\nawait moveFile(\"bestiary/dragon\", \"moonstone-dragon-wyrmling\", \"moonstone-dragon-wyrmling-ftd\");\nawait moveFile(\"bestiary/dragon\", \"red-greatwyrm\", \"red-greatwyrm-ftd\");\nawait moveFile(\"bestiary/dragon\", \"reduced-threat-dragon-turtle\", \"reduced-threat-dragon-turtle-tftyp\");\nawait moveFile(\"bestiary/dragon\", \"reduced-threat-wyvern\", \"reduced-threat-wyvern-tftyp\");\nawait moveFile(\"bestiary/dragon\", \"sapphire-dragon-wyrmling\", \"sapphire-dragon-wyrmling-ftd\");\nawait moveFile(\"bestiary/dragon\", \"sapphire-greatwyrm\", \"sapphire-greatwyrm-ftd\");\nawait moveFile(\"bestiary/dragon\", \"silver-greatwyrm\", \"silver-greatwyrm-ftd\");\nawait moveFile(\"bestiary/dragon\", \"solar-dragon-wyrmling\", \"solar-dragon-wyrmling-bam\");\nawait moveFile(\"bestiary/dragon\", \"topaz-dragon-wyrmling\", \"topaz-dragon-wyrmling-ftd\");\nawait moveFile(\"bestiary/dragon\", \"topaz-greatwyrm\", \"topaz-greatwyrm-ftd\");\nawait moveFile(\"bestiary/dragon\", \"white-greatwyrm\", \"white-greatwyrm-ftd\");\nawait moveFile(\"bestiary/dragon\", \"young-amethyst-dragon\", \"young-amethyst-dragon-ftd\");\nawait moveFile(\"bestiary/dragon\", \"young-amonkhet-dragon\", \"young-amonkhet-dragon-psa\");\nawait moveFile(\"bestiary/dragon\", \"young-crystal-dragon\", \"young-crystal-dragon-ftd\");\nawait moveFile(\"bestiary/dragon\", \"young-deep-dragon\", \"young-deep-dragon-ftd\");\nawait moveFile(\"bestiary/dragon\", \"young-dragon-turtle\", \"young-dragon-turtle-ftd\");\nawait moveFile(\"bestiary/dragon\", \"young-emerald-dragon\", \"young-emerald-dragon-ftd\");\nawait moveFile(\"bestiary/dragon\", \"young-lunar-dragon\", \"young-lunar-dragon-bam\");\nawait moveFile(\"bestiary/dragon\", \"young-moonstone-dragon\", \"young-moonstone-dragon-ftd\");\nawait moveFile(\"bestiary/dragon\", \"young-sapphire-dragon\", \"young-sapphire-dragon-ftd\");\nawait moveFile(\"bestiary/dragon\", \"young-sea-serpent\", \"young-sea-serpent-ftd\");\nawait moveFile(\"bestiary/dragon\", \"young-solar-dragon\", \"young-solar-dragon-bam\");\nawait moveFile(\"bestiary/dragon\", \"young-topaz-dragon\", \"young-topaz-dragon-ftd\");\nawait moveFile(\"bestiary/dragon\", \"zendikar-dragon\", \"zendikar-dragon-psz\");\nawait moveFile(\"bestiary/elemental\", \"air-elemental-myrmidon\", \"air-elemental-myrmidon-mpmm\");\nawait moveFile(\"bestiary/elemental\", \"animated-breath\", \"animated-breath-ftd\");\nawait moveFile(\"bestiary/elemental\", \"animated-drow-statue\", \"animated-drow-statue-oota\");\nawait moveFile(\"bestiary/elemental\", \"arclight-phoenix\", \"arclight-phoenix-ggr\");\nawait moveFile(\"bestiary/elemental\", \"art-elemental-mascot\", \"art-elemental-mascot-scc\");\nawait moveFile(\"bestiary/elemental\", \"big-xorn\", \"big-xorn-wdmm\");\nawait moveFile(\"bestiary/elemental\", \"blistercoil-weird\", \"blistercoil-weird-ggr\");\nawait moveFile(\"bestiary/elemental\", \"chwinga-astronaut\", \"chwinga-astronaut-bam\");\nawait moveFile(\"bestiary/elemental\", \"chwinga\", \"chwinga-toa\");\nawait moveFile(\"bestiary/elemental\", \"earth-elemental-myrmidon\", \"earth-elemental-myrmidon-mpmm\");\nawait moveFile(\"bestiary/elemental\", \"elder-tempest\", \"elder-tempest-mpmm\");\nawait moveFile(\"bestiary/elemental\", \"elemental-spirit-4th-level-spell\", \"elemental-spirit-4th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/elemental\", \"elemental-spirit-5th-level-spell\", \"elemental-spirit-5th-level-spell-tce\");\nawait moveFile(\"bestiary/elemental\", \"elemental-spirit-6th-level-spell\", \"elemental-spirit-6th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/elemental\", \"elemental-spirit-7th-level-spell\", \"elemental-spirit-7th-level-spell-tce\");\nawait moveFile(\"bestiary/elemental\", \"elemental-spirit-8th-level-spell\", \"elemental-spirit-8th-level-spell-tce\");\nawait moveFile(\"bestiary/elemental\", \"elemental-spirit-9th-level-spell\", \"elemental-spirit-9th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/elemental\", \"fire-elemental-myrmidon\", \"fire-elemental-myrmidon-mpmm\");\nawait moveFile(\"bestiary/elemental\", \"firenewt-warlock-of-imix\", \"firenewt-warlock-of-imix-mpmm\");\nawait moveFile(\"bestiary/elemental\", \"firenewt-warrior\", \"firenewt-warrior-mpmm\");\nawait moveFile(\"bestiary/elemental\", \"flail-snail\", \"flail-snail-mpmm\");\nawait moveFile(\"bestiary/elemental\", \"fluxcharger\", \"fluxcharger-ggr\");\nawait moveFile(\"bestiary/elemental\", \"four-armed-gargoyle\", \"four-armed-gargoyle-tftyp\");\nawait moveFile(\"bestiary/elemental\", \"frost-salamander\", \"frost-salamander-mpmm\");\nawait moveFile(\"bestiary/elemental\", \"fume-drake\", \"fume-drake-dosi\");\nawait moveFile(\"bestiary/elemental\", \"galvanice-weird\", \"galvanice-weird-ggr\");\nawait moveFile(\"bestiary/elemental\", \"geonid\", \"geonid-ttp\");\nawait moveFile(\"bestiary/elemental\", \"giant-four-armed-gargoyle\", \"giant-four-armed-gargoyle-toa\");\nawait moveFile(\"bestiary/elemental\", \"giant-strider\", \"giant-strider-mpmm\");\nawait moveFile(\"bestiary/elemental\", \"khargra\", \"khargra-mff\");\nawait moveFile(\"bestiary/elemental\", \"leviathan\", \"leviathan-mpmm\");\nawait moveFile(\"bestiary/elemental\", \"murder-comet\", \"murder-comet-bam\");\nawait moveFile(\"bestiary/elemental\", \"phoenix\", \"phoenix-mpmm\");\nawait moveFile(\"bestiary/elemental\", \"poison-weird\", \"poison-weird-wdmm\");\nawait moveFile(\"bestiary/elemental\", \"statue-of-talos\", \"statue-of-talos-slw\");\nawait moveFile(\"bestiary/elemental\", \"tlexolotl\", \"tlexolotl-jttrc\");\nawait moveFile(\"bestiary/elemental\", \"water-elemental-myrmidon\", \"water-elemental-myrmidon-mpmm\");\nawait moveFile(\"bestiary/elemental\", \"wildfire-spirit\", \"wildfire-spirit-tce\");\nawait moveFile(\"bestiary/elemental\", \"zaratan\", \"zaratan-mpmm\");\nawait moveFile(\"bestiary/fey\", \"alseid\", \"alseid-mot\");\nawait moveFile(\"bestiary/fey\", \"annis-hag\", \"annis-hag-mpmm\");\nawait moveFile(\"bestiary/fey\", \"autumn-eladrin\", \"autumn-eladrin-mpmm\");\nawait moveFile(\"bestiary/fey\", \"bheur-hag\", \"bheur-hag-mpmm\");\nawait moveFile(\"bestiary/fey\", \"boggle\", \"boggle-mpmm\");\nawait moveFile(\"bestiary/fey\", \"brigganock\", \"brigganock-wbtw\");\nawait moveFile(\"bestiary/fey\", \"conclave-dryad\", \"conclave-dryad-ggr\");\nawait moveFile(\"bestiary/fey\", \"dankwood-hag\", \"dankwood-hag-awm\");\nawait moveFile(\"bestiary/fey\", \"darkling-elder\", \"darkling-elder-mpmm\");\nawait moveFile(\"bestiary/fey\", \"darkling\", \"darkling-mpmm\");\nawait moveFile(\"bestiary/fey\", \"detached-shadow\", \"detached-shadow-wbtw\");\nawait moveFile(\"bestiary/fey\", \"dohwar\", \"dohwar-bam\");\nawait moveFile(\"bestiary/fey\", \"dolphin-delighter\", \"dolphin-delighter-mpmm\");\nawait moveFile(\"bestiary/fey\", \"dusk-hag\", \"dusk-hag-erlw\");\nawait moveFile(\"bestiary/fey\", \"fey-spirit-3rd-level-spell\", \"fey-spirit-3rd-level-spell-tce\");\nawait moveFile(\"bestiary/fey\", \"fey-spirit-4th-level-spell\", \"fey-spirit-4th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/fey\", \"fey-spirit-5th-level-spell\", \"fey-spirit-5th-level-spell-tce\");\nawait moveFile(\"bestiary/fey\", \"fey-spirit-6th-level-spell\", \"fey-spirit-6th-level-spell-tce\");\nawait moveFile(\"bestiary/fey\", \"fey-spirit-7th-level-spell\", \"fey-spirit-7th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/fey\", \"fey-spirit-8th-level-spell\", \"fey-spirit-8th-level-spell-tce\");\nawait moveFile(\"bestiary/fey\", \"fey-spirit-9th-level-spell\", \"fey-spirit-9th-level-spell-tce\");\nawait moveFile(\"bestiary/fey\", \"forlarren\", \"forlarren-mff\");\nawait moveFile(\"bestiary/fey\", \"hag-of-the-fetid-gaze\", \"hag-of-the-fetid-gaze-cm\");\nawait moveFile(\"bestiary/fey\", \"hamadryad\", \"hamadryad-mabjov\");\nawait moveFile(\"bestiary/fey\", \"hobgoblin-devastator\", \"hobgoblin-devastator-mpmm\");\nawait moveFile(\"bestiary/fey\", \"hobgoblin-iron-shadow\", \"hobgoblin-iron-shadow-mpmm\");\nawait moveFile(\"bestiary/fey\", \"killmoulis\", \"killmoulis-mff\");\nawait moveFile(\"bestiary/fey\", \"korred\", \"korred-mpmm\");\nawait moveFile(\"bestiary/fey\", \"lampad\", \"lampad-mot\");\nawait moveFile(\"bestiary/fey\", \"meenlock\", \"meenlock-mpmm\");\nawait moveFile(\"bestiary/fey\", \"mite\", \"mite-mff\");\nawait moveFile(\"bestiary/fey\", \"naiad\", \"naiad-cm\");\nawait moveFile(\"bestiary/fey\", \"nereid\", \"nereid-tftyp\");\nawait moveFile(\"bestiary/fey\", \"nilbog\", \"nilbog-mpmm\");\nawait moveFile(\"bestiary/fey\", \"oread\", \"oread-mot\");\nawait moveFile(\"bestiary/fey\", \"quickling\", \"quickling-mpmm\");\nawait moveFile(\"bestiary/fey\", \"redcap\", \"redcap-mpmm\");\nawait moveFile(\"bestiary/fey\", \"reflection\", \"reflection-tce\");\nawait moveFile(\"bestiary/fey\", \"riverine\", \"riverine-jttrc\");\nawait moveFile(\"bestiary/fey\", \"satyr-reveler\", \"satyr-reveler-mot\");\nawait moveFile(\"bestiary/fey\", \"satyr-thornbearer\", \"satyr-thornbearer-mot\");\nawait moveFile(\"bestiary/fey\", \"screaming-devilkin\", \"screaming-devilkin-mff\");\nawait moveFile(\"bestiary/fey\", \"sea-fury\", \"sea-fury-egw\");\nawait moveFile(\"bestiary/fey\", \"sirene\", \"sirene-mabjov\");\nawait moveFile(\"bestiary/fey\", \"sludge-hag\", \"sludge-hag-mgelft\");\nawait moveFile(\"bestiary/fey\", \"spring-eladrin\", \"spring-eladrin-mpmm\");\nawait moveFile(\"bestiary/fey\", \"summer-eladrin\", \"summer-eladrin-mpmm\");\nawait moveFile(\"bestiary/fey\", \"valenar-hawk\", \"valenar-hawk-erlw\");\nawait moveFile(\"bestiary/fey\", \"valenar-hound\", \"valenar-hound-erlw\");\nawait moveFile(\"bestiary/fey\", \"valenar-steed\", \"valenar-steed-erlw\");\nawait moveFile(\"bestiary/fey\", \"winter-eladrin\", \"winter-eladrin-mpmm\");\nawait moveFile(\"bestiary/fey\", \"wynling\", \"wynling-jttrc\");\nawait moveFile(\"bestiary/fey\", \"yeth-hound\", \"yeth-hound-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"abhorrent-overlord\", \"abhorrent-overlord-mot\");\nawait moveFile(\"bestiary/fiend\", \"abyssal-chicken\", \"abyssal-chicken-bgdia\");\nawait moveFile(\"bestiary/fiend\", \"abyssal-wretch\", \"abyssal-wretch-mtf\");\nawait moveFile(\"bestiary/fiend\", \"alkilith\", \"alkilith-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"amnizu\", \"amnizu-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"archfiend-of-ifnir\", \"archfiend-of-ifnir-psa\");\nawait moveFile(\"bestiary/fiend\", \"armanite\", \"armanite-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"babau\", \"babau-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"barghest\", \"barghest-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"bebilith\", \"bebilith-mabjov\");\nawait moveFile(\"bestiary/fiend\", \"black-abishai\", \"black-abishai-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"blue-abishai\", \"blue-abishai-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"bulezau\", \"bulezau-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"cackler\", \"cackler-ggr\");\nawait moveFile(\"bestiary/fiend\", \"canoloth\", \"canoloth-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"chardalyn-berserker\", \"chardalyn-berserker-idrotf\");\nawait moveFile(\"bestiary/fiend\", \"daemogoth\", \"daemogoth-scc\");\nawait moveFile(\"bestiary/fiend\", \"daemogoth-titan\", \"daemogoth-titan-scc\");\nawait moveFile(\"bestiary/fiend\", \"dancing-flame\", \"dancing-flame-crcotn\");\nawait moveFile(\"bestiary/fiend\", \"demodand-farastu\", \"demodand-farastu-mabjov\");\nawait moveFile(\"bestiary/fiend\", \"demodand-kelubar\", \"demodand-kelubar-mabjov\");\nawait moveFile(\"bestiary/fiend\", \"demodand-shator\", \"demodand-shator-mabjov\");\nawait moveFile(\"bestiary/fiend\", \"demonlord-of-ashmouth\", \"demonlord-of-ashmouth-psi\");\nawait moveFile(\"bestiary/fiend\", \"dhergoloth\", \"dhergoloth-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"draegloth\", \"draegloth-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"dybbuk\", \"dybbuk-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"eater-of-hope\", \"eater-of-hope-mot\");\nawait moveFile(\"bestiary/fiend\", \"fiendish-orc\", \"fiendish-orc-crcotn\");\nawait moveFile(\"bestiary/fiend\", \"fiendish-spirit-6th-level-spell\", \"fiendish-spirit-6th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/fiend\", \"fiendish-spirit-7th-level-spell\", \"fiendish-spirit-7th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/fiend\", \"fiendish-spirit-8th-level-spell\", \"fiendish-spirit-8th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/fiend\", \"fiendish-spirit-9th-level-spell\", \"fiendish-spirit-9th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/fiend\", \"fiendish-spirit-demon-6th-level-spell\", \"fiendish-spirit-demon-6th-level-spell-tce\");\nawait moveFile(\"bestiary/fiend\", \"fiendish-spirit-demon-7th-level-spell\", \"fiendish-spirit-demon-7th-level-spell-tce\");\nawait moveFile(\"bestiary/fiend\", \"fiendish-spirit-demon-8th-level-spell\", \"fiendish-spirit-demon-8th-level-spell-tce\");\nawait moveFile(\"bestiary/fiend\", \"fiendish-spirit-demon-9th-level-spell\", \"fiendish-spirit-demon-9th-level-spell-tce\");\nawait moveFile(\"bestiary/fiend\", \"fiendish-spirit-devil-6th-level-spell\", \"fiendish-spirit-devil-6th-level-spell-tce\");\nawait moveFile(\"bestiary/fiend\", \"fiendish-spirit-devil-7th-level-spell\", \"fiendish-spirit-devil-7th-level-spell-tce\");\nawait moveFile(\"bestiary/fiend\", \"fiendish-spirit-devil-8th-level-spell\", \"fiendish-spirit-devil-8th-level-spell-tce\");\nawait moveFile(\"bestiary/fiend\", \"fiendish-spirit-devil-9th-level-spell\", \"fiendish-spirit-devil-9th-level-spell-tce\");\nawait moveFile(\"bestiary/fiend\", \"fiendish-spirit-yugoloth-6th-level-spell\", \"fiendish-spirit-yugoloth-6th-level-spell-tce\");\nawait moveFile(\"bestiary/fiend\", \"fiendish-spirit-yugoloth-7th-level-spell\", \"fiendish-spirit-yugoloth-7th-level-spell-tce\");\nawait moveFile(\"bestiary/fiend\", \"fiendish-spirit-yugoloth-8th-level-spell\", \"fiendish-spirit-yugoloth-8th-level-spell-tce\");\nawait moveFile(\"bestiary/fiend\", \"fiendish-spirit-yugoloth-9th-level-spell\", \"fiendish-spirit-yugoloth-9th-level-spell-tce\");\nawait moveFile(\"bestiary/fiend\", \"flind\", \"flind-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"green-abishai\", \"green-abishai-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"hellwasp\", \"hellwasp-bgdia\");\nawait moveFile(\"bestiary/fiend\", \"howler\", \"howler-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"hydroloth\", \"hydroloth-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"master-of-cruelties\", \"master-of-cruelties-ggr\");\nawait moveFile(\"bestiary/fiend\", \"maurezhi\", \"maurezhi-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"maw-demon\", \"maw-demon-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"merregon\", \"merregon-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"merrenoloth\", \"merrenoloth-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"molydeus\", \"molydeus-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"nabassu\", \"nabassu-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"narzugon\", \"narzugon-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"nergaliid\", \"nergaliid-egw\");\nawait moveFile(\"bestiary/fiend\", \"nightmare-shepherd\", \"nightmare-shepherd-mot\");\nawait moveFile(\"bestiary/fiend\", \"nupperibo\", \"nupperibo-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"oinoloth\", \"oinoloth-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"orthon\", \"orthon-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"pestilence-demon\", \"pestilence-demon-psz\");\nawait moveFile(\"bestiary/fiend\", \"red-abishai\", \"red-abishai-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"reduced-threat-glabrezu\", \"reduced-threat-glabrezu-tftyp\");\nawait moveFile(\"bestiary/fiend\", \"reduced-threat-hezrou\", \"reduced-threat-hezrou-tftyp\");\nawait moveFile(\"bestiary/fiend\", \"reduced-threat-vrock\", \"reduced-threat-vrock-tftyp\");\nawait moveFile(\"bestiary/fiend\", \"relentless-juggernaut\", \"relentless-juggernaut-vrgr\");\nawait moveFile(\"bestiary/fiend\", \"relentless-slasher\", \"relentless-slasher-vrgr\");\nawait moveFile(\"bestiary/fiend\", \"rutterkin\", \"rutterkin-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"shoosuva\", \"shoosuva-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"sibriex\", \"sibriex-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"sire-of-insanity\", \"sire-of-insanity-ggr\");\nawait moveFile(\"bestiary/fiend\", \"soulstinger-demon\", \"soulstinger-demon-psa\");\nawait moveFile(\"bestiary/fiend\", \"space-clown\", \"space-clown-bam\");\nawait moveFile(\"bestiary/fiend\", \"stench-kow\", \"stench-kow-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"tanarukk\", \"tanarukk-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"tlacatecolo\", \"tlacatecolo-jttrc\");\nawait moveFile(\"bestiary/fiend\", \"udaak\", \"udaak-egw\");\nawait moveFile(\"bestiary/fiend\", \"vargouille\", \"vargouille-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"wastrilith\", \"wastrilith-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"white-abishai\", \"white-abishai-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"yagnoloth\", \"yagnoloth-mpmm\");\nawait moveFile(\"bestiary/fiend\", \"zakya-rakshasa\", \"zakya-rakshasa-erlw\");\nawait moveFile(\"bestiary/giant\", \"aquatic-troll\", \"aquatic-troll-rot\");\nawait moveFile(\"bestiary/giant\", \"bloodfray-giant\", \"bloodfray-giant-ggr\");\nawait moveFile(\"bestiary/giant\", \"boulderfoot-giant\", \"boulderfoot-giant-psz\");\nawait moveFile(\"bestiary/giant\", \"braxat\", \"braxat-bam\");\nawait moveFile(\"bestiary/giant\", \"brohg\", \"brohg-bam\");\nawait moveFile(\"bestiary/giant\", \"carrion-ogre\", \"carrion-ogre-wdmm\");\nawait moveFile(\"bestiary/giant\", \"cloud-giant-noble\", \"cloud-giant-noble-tftyp\");\nawait moveFile(\"bestiary/giant\", \"cloud-giant-smiling-one\", \"cloud-giant-smiling-one-mpmm\");\nawait moveFile(\"bestiary/giant\", \"crab-folk\", \"crab-folk-mff\");\nawait moveFile(\"bestiary/giant\", \"deadstone-cleft-stone-giant\", \"deadstone-cleft-stone-giant-skt\");\nawait moveFile(\"bestiary/giant\", \"dire-troll\", \"dire-troll-mpmm\");\nawait moveFile(\"bestiary/giant\", \"doomwake-giant\", \"doomwake-giant-mot\");\nawait moveFile(\"bestiary/giant\", \"fire-giant-dreadnought\", \"fire-giant-dreadnought-mpmm\");\nawait moveFile(\"bestiary/giant\", \"fire-giant-royal-headsman\", \"fire-giant-royal-headsman-tftyp\");\nawait moveFile(\"bestiary/giant\", \"fire-giant-servant\", \"fire-giant-servant-tftyp\");\nawait moveFile(\"bestiary/giant\", \"five-armed-troll\", \"five-armed-troll-wdmm\");\nawait moveFile(\"bestiary/giant\", \"fog-giant\", \"fog-giant-mff\");\nawait moveFile(\"bestiary/giant\", \"four-armed-troll\", \"four-armed-troll-hotdq\");\nawait moveFile(\"bestiary/giant\", \"frost-giant-everlasting-one\", \"frost-giant-everlasting-one-mpmm\");\nawait moveFile(\"bestiary/giant\", \"frost-giant-servant\", \"frost-giant-servant-tftyp\");\nawait moveFile(\"bestiary/giant\", \"giant-mutated-drow\", \"giant-mutated-drow-wdmm\");\nawait moveFile(\"bestiary/giant\", \"guardian-giant\", \"guardian-giant-ggr\");\nawait moveFile(\"bestiary/giant\", \"hill-giant-sergeant\", \"hill-giant-sergeant-tftyp\");\nawait moveFile(\"bestiary/giant\", \"hill-giant-servant\", \"hill-giant-servant-tftyp\");\nawait moveFile(\"bestiary/giant\", \"hill-giant-subchief\", \"hill-giant-subchief-tftyp\");\nawait moveFile(\"bestiary/giant\", \"hundred-handed-one\", \"hundred-handed-one-mot\");\nawait moveFile(\"bestiary/giant\", \"hurda\", \"hurda-psz\");\nawait moveFile(\"bestiary/giant\", \"ice-troll\", \"ice-troll-rot\");\nawait moveFile(\"bestiary/giant\", \"mercane\", \"mercane-bam\");\nawait moveFile(\"bestiary/giant\", \"mouth-of-grolantor\", \"mouth-of-grolantor-mpmm\");\nawait moveFile(\"bestiary/giant\", \"nivix-cyclops\", \"nivix-cyclops-ggr\");\nawait moveFile(\"bestiary/giant\", \"ogre-battering-ram\", \"ogre-battering-ram-mpmm\");\nawait moveFile(\"bestiary/giant\", \"ogre-bolt-launcher\", \"ogre-bolt-launcher-mpmm\");\nawait moveFile(\"bestiary/giant\", \"ogre-chain-brute\", \"ogre-chain-brute-mpmm\");\nawait moveFile(\"bestiary/giant\", \"ogre-channeler\", \"ogre-channeler-psz\");\nawait moveFile(\"bestiary/giant\", \"ogre-goblin-hucker\", \"ogre-goblin-hucker-skt\");\nawait moveFile(\"bestiary/giant\", \"ogre-howdah\", \"ogre-howdah-mpmm\");\nawait moveFile(\"bestiary/giant\", \"orzhov-giant\", \"orzhov-giant-ggr\");\nawait moveFile(\"bestiary/giant\", \"rot-troll\", \"rot-troll-mpmm\");\nawait moveFile(\"bestiary/giant\", \"scrag\", \"scrag-tftyp\");\nawait moveFile(\"bestiary/giant\", \"shatterskull-giant\", \"shatterskull-giant-psz\");\nawait moveFile(\"bestiary/giant\", \"spirit-troll\", \"spirit-troll-mpmm\");\nawait moveFile(\"bestiary/giant\", \"stone-giant-dreamwalker\", \"stone-giant-dreamwalker-mpmm\");\nawait moveFile(\"bestiary/giant\", \"storm-giant-quintessent\", \"storm-giant-quintessent-mpmm\");\nawait moveFile(\"bestiary/giant\", \"sunder-shaman\", \"sunder-shaman-ggr\");\nawait moveFile(\"bestiary/giant\", \"the-bagman\", \"the-bagman-vrgr\");\nawait moveFile(\"bestiary/giant\", \"trench-giant\", \"trench-giant-psz\");\nawait moveFile(\"bestiary/giant\", \"turntimber-giant\", \"turntimber-giant-psz\");\nawait moveFile(\"bestiary/giant\", \"venom-troll\", \"venom-troll-mpmm\");\nawait moveFile(\"bestiary/giant\", \"verbeeg-longstrider\", \"verbeeg-longstrider-idrotf\");\nawait moveFile(\"bestiary/giant\", \"verbeeg-marauder\", \"verbeeg-marauder-idrotf\");\nawait moveFile(\"bestiary/giant\", \"young-cloud-giant\", \"young-cloud-giant-skt\");\nawait moveFile(\"bestiary/giant\", \"young-fire-giant\", \"young-fire-giant-tftyp\");\nawait moveFile(\"bestiary/giant\", \"young-frost-giant\", \"young-frost-giant-tftyp\");\nawait moveFile(\"bestiary/giant\", \"young-hill-giant\", \"young-hill-giant-tftyp\");\nawait moveFile(\"bestiary/giant\", \"young-ogre-servant\", \"young-ogre-servant-tftyp\");\nawait moveFile(\"bestiary/humanoid\", \"aarakocra-simulacrum\", \"aarakocra-simulacrum-skt\");\nawait moveFile(\"bestiary/humanoid\", \"aarakocra-spelljammer\", \"aarakocra-spelljammer-lox\");\nawait moveFile(\"bestiary/humanoid\", \"abjurer-wizard\", \"abjurer-wizard-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"akroan-hoplite\", \"akroan-hoplite-mot\");\nawait moveFile(\"bestiary/humanoid\", \"albino-dwarf-spirit-warrior\", \"albino-dwarf-spirit-warrior-toa\");\nawait moveFile(\"bestiary/humanoid\", \"albino-dwarf-warrior\", \"albino-dwarf-warrior-toa\");\nawait moveFile(\"bestiary/humanoid\", \"anarch\", \"anarch-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"anchorite-of-talos\", \"anchorite-of-talos-dip\");\nawait moveFile(\"bestiary/humanoid\", \"apprentice\", \"apprentice-hol\");\nawait moveFile(\"bestiary/humanoid\", \"apprentice-wizard\", \"apprentice-wizard-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"archdruid\", \"archdruid-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"archer\", \"archer-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"ashen-heir-anarchist\", \"ashen-heir-anarchist-jttrc\");\nawait moveFile(\"bestiary/humanoid\", \"ashen-heir-assassin\", \"ashen-heir-assassin-jttrc\");\nawait moveFile(\"bestiary/humanoid\", \"ashen-heir-mage\", \"ashen-heir-mage-jttrc\");\nawait moveFile(\"bestiary/humanoid\", \"ashen-heir-veteran\", \"ashen-heir-veteran-jttrc\");\nawait moveFile(\"bestiary/humanoid\", \"astral-elf-aristocrat\", \"astral-elf-aristocrat-bam\");\nawait moveFile(\"bestiary/humanoid\", \"astral-elf-commander\", \"astral-elf-commander-bam\");\nawait moveFile(\"bestiary/humanoid\", \"astral-elf-honor-guard\", \"astral-elf-honor-guard-bam\");\nawait moveFile(\"bestiary/humanoid\", \"astral-elf-star-priest\", \"astral-elf-star-priest-bam\");\nawait moveFile(\"bestiary/humanoid\", \"astral-elf-warrior\", \"astral-elf-warrior-bam\");\nawait moveFile(\"bestiary/humanoid\", \"axe-of-mirabar-soldier\", \"axe-of-mirabar-soldier-skt\");\nawait moveFile(\"bestiary/humanoid\", \"bard\", \"bard-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"barovian-commoner\", \"barovian-commoner-cos\");\nawait moveFile(\"bestiary/humanoid\", \"barovian-scout\", \"barovian-scout-cos\");\nawait moveFile(\"bestiary/humanoid\", \"barovian-witch\", \"barovian-witch-cos\");\nawait moveFile(\"bestiary/humanoid\", \"battlehammer-dwarf\", \"battlehammer-dwarf-idrotf\");\nawait moveFile(\"bestiary/humanoid\", \"bhaal-slayer\", \"bhaal-slayer-mabjov\");\nawait moveFile(\"bestiary/humanoid\", \"biomancer\", \"biomancer-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"black-earth-guard\", \"black-earth-guard-pota\");\nawait moveFile(\"bestiary/humanoid\", \"black-earth-priest\", \"black-earth-priest-pota\");\nawait moveFile(\"bestiary/humanoid\", \"black-gauntlet-of-bane\", \"black-gauntlet-of-bane-bgdia\");\nawait moveFile(\"bestiary/humanoid\", \"blackguard\", \"blackguard-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"blindheim\", \"blindheim-mff\");\nawait moveFile(\"bestiary/humanoid\", \"blood-hunter\", \"blood-hunter-egw\");\nawait moveFile(\"bestiary/humanoid\", \"blood-witch\", \"blood-witch-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"bone-knight\", \"bone-knight-erlw\");\nawait moveFile(\"bestiary/humanoid\", \"booyahg-booyahg-booyahg\", \"booyahg-booyahg-booyahg-vgm\");\nawait moveFile(\"bestiary/humanoid\", \"booyahg-caster\", \"booyahg-caster-vgm\");\nawait moveFile(\"bestiary/humanoid\", \"booyahg-slave-of-the-archfey\", \"booyahg-slave-of-the-archfey-vgm\");\nawait moveFile(\"bestiary/humanoid\", \"booyahg-slave-of-the-fiend\", \"booyahg-slave-of-the-fiend-vgm\");\nawait moveFile(\"bestiary/humanoid\", \"booyahg-slave-of-the-great-old-one\", \"booyahg-slave-of-the-great-old-one-vgm\");\nawait moveFile(\"bestiary/humanoid\", \"booyahg-whip\", \"booyahg-whip-vgm\");\nawait moveFile(\"bestiary/humanoid\", \"booyahg-wielder\", \"booyahg-wielder-vgm\");\nawait moveFile(\"bestiary/humanoid\", \"bugbear-gardener\", \"bugbear-gardener-tftyp\");\nawait moveFile(\"bestiary/humanoid\", \"bugbear-lieutenant\", \"bugbear-lieutenant-tftyp\");\nawait moveFile(\"bestiary/humanoid\", \"bullywug-croaker\", \"bullywug-croaker-gos\");\nawait moveFile(\"bestiary/humanoid\", \"bullywug-knight\", \"bullywug-knight-wbtw\");\nawait moveFile(\"bestiary/humanoid\", \"bullywug-royal\", \"bullywug-royal-gos\");\nawait moveFile(\"bestiary/humanoid\", \"burrowshark\", \"burrowshark-pota\");\nawait moveFile(\"bestiary/humanoid\", \"champion\", \"champion-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"changeling\", \"changeling-erlw\");\nawait moveFile(\"bestiary/humanoid\", \"clown\", \"clown-nrh-coi\");\nawait moveFile(\"bestiary/humanoid\", \"conjurer-wizard\", \"conjurer-wizard-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"cosmotronic-blastseeker\", \"cosmotronic-blastseeker-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"counterflux-blastseeker\", \"counterflux-blastseeker-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"crushing-wave-priest\", \"crushing-wave-priest-pota\");\nawait moveFile(\"bestiary/humanoid\", \"crushing-wave-reaver\", \"crushing-wave-reaver-pota\");\nawait moveFile(\"bestiary/humanoid\", \"crystal-cave-merfolk\", \"crystal-cave-merfolk-awm\");\nawait moveFile(\"bestiary/humanoid\", \"dankwood-duergar\", \"dankwood-duergar-mgelft\");\nawait moveFile(\"bestiary/humanoid\", \"dankwood-grung\", \"dankwood-grung-mgelft\");\nawait moveFile(\"bestiary/humanoid\", \"dark-tide-knight\", \"dark-tide-knight-pota\");\nawait moveFile(\"bestiary/humanoid\", \"deaths-head-of-bhaal\", \"deaths-head-of-bhaal-bgdia\");\nawait moveFile(\"bestiary/humanoid\", \"dire-corby\", \"dire-corby-mff\");\nawait moveFile(\"bestiary/humanoid\", \"disciple\", \"disciple-hol\");\nawait moveFile(\"bestiary/humanoid\", \"distended-corpse\", \"distended-corpse-cos\");\nawait moveFile(\"bestiary/humanoid\", \"diva\", \"diva-jttrc\");\nawait moveFile(\"bestiary/humanoid\", \"diviner-wizard\", \"diviner-wizard-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"dragon-blessed\", \"dragon-blessed-ftd\");\nawait moveFile(\"bestiary/humanoid\", \"dragon-chosen\", \"dragon-chosen-ftd\");\nawait moveFile(\"bestiary/humanoid\", \"dragon-hunter\", \"dragon-hunter-jttrc\");\nawait moveFile(\"bestiary/humanoid\", \"dragon-speaker\", \"dragon-speaker-ftd\");\nawait moveFile(\"bestiary/humanoid\", \"dragonborn-of-bahamut\", \"dragonborn-of-bahamut-ftd\");\nawait moveFile(\"bestiary/humanoid\", \"dragonborn-of-sardior\", \"dragonborn-of-sardior-ftd\");\nawait moveFile(\"bestiary/humanoid\", \"dragonborn-of-tiamat\", \"dragonborn-of-tiamat-ftd\");\nawait moveFile(\"bestiary/humanoid\", \"dragonclaw\", \"dragonclaw-hotdq\");\nawait moveFile(\"bestiary/humanoid\", \"dragonfang\", \"dragonfang-rot\");\nawait moveFile(\"bestiary/humanoid\", \"dragonsoul\", \"dragonsoul-rot\");\nawait moveFile(\"bestiary/humanoid\", \"dragonwing\", \"dragonwing-hotdq\");\nawait moveFile(\"bestiary/humanoid\", \"drow-acolyte\", \"drow-acolyte-oota\");\nawait moveFile(\"bestiary/humanoid\", \"drow-arachnomancer\", \"drow-arachnomancer-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"drow-bandit\", \"drow-bandit-oota\");\nawait moveFile(\"bestiary/humanoid\", \"drow-commander\", \"drow-commander-tftyp\");\nawait moveFile(\"bestiary/humanoid\", \"drow-commoner\", \"drow-commoner-oota\");\nawait moveFile(\"bestiary/humanoid\", \"drow-cultist\", \"drow-cultist-oota\");\nawait moveFile(\"bestiary/humanoid\", \"drow-favored-consort\", \"drow-favored-consort-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"drow-guard\", \"drow-guard-oota\");\nawait moveFile(\"bestiary/humanoid\", \"drow-gunslinger\", \"drow-gunslinger-wdh\");\nawait moveFile(\"bestiary/humanoid\", \"drow-house-captain\", \"drow-house-captain-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"drow-inquisitor\", \"drow-inquisitor-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"drow-matron-mother\", \"drow-matron-mother-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"drow-noble\", \"drow-noble-oota\");\nawait moveFile(\"bestiary/humanoid\", \"drow-scout\", \"drow-scout-oota\");\nawait moveFile(\"bestiary/humanoid\", \"drow-shadowblade\", \"drow-shadowblade-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"drow-spy\", \"drow-spy-oota\");\nawait moveFile(\"bestiary/humanoid\", \"druid-of-the-old-ways\", \"druid-of-the-old-ways-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"duergar-alchemist\", \"duergar-alchemist-oota\");\nawait moveFile(\"bestiary/humanoid\", \"duergar-darkhaft\", \"duergar-darkhaft-oota\");\nawait moveFile(\"bestiary/humanoid\", \"duergar-despot\", \"duergar-despot-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"duergar-kavalrachni\", \"duergar-kavalrachni-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"duergar-keeper-of-the-flame\", \"duergar-keeper-of-the-flame-oota\");\nawait moveFile(\"bestiary/humanoid\", \"duergar-mind-master\", \"duergar-mind-master-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"duergar-soulblade\", \"duergar-soulblade-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"duergar-spy\", \"duergar-spy-tftyp\");\nawait moveFile(\"bestiary/humanoid\", \"duergar-stone-guard\", \"duergar-stone-guard-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"duergar-warlord\", \"duergar-warlord-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"duergar-xarrorn\", \"duergar-xarrorn-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"dum-dum-goblin\", \"dum-dum-goblin-awm\");\nawait moveFile(\"bestiary/humanoid\", \"dwarf\", \"dwarf-awm\");\nawait moveFile(\"bestiary/humanoid\", \"dwarven-worker\", \"dwarven-worker-imr\");\nawait moveFile(\"bestiary/humanoid\", \"elder-monastery-of-the-distressed-body-monk\", \"elder-monastery-of-the-distressed-body-monk-llk\");\nawait moveFile(\"bestiary/humanoid\", \"emerald-enclave-scout\", \"emerald-enclave-scout-oota\");\nawait moveFile(\"bestiary/humanoid\", \"enchanter-wizard\", \"enchanter-wizard-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"engineer\", \"engineer-wdh\");\nawait moveFile(\"bestiary/humanoid\", \"eternal-flame-guardian\", \"eternal-flame-guardian-pota\");\nawait moveFile(\"bestiary/humanoid\", \"eternal-flame-priest\", \"eternal-flame-priest-pota\");\nawait moveFile(\"bestiary/humanoid\", \"evil-mage\", \"evil-mage-lmop\");\nawait moveFile(\"bestiary/humanoid\", \"evoker-wizard\", \"evoker-wizard-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"expert\", \"expert-sdw\");\nawait moveFile(\"bestiary/humanoid\", \"fathomer\", \"fathomer-pota\");\nawait moveFile(\"bestiary/humanoid\", \"feathergale-knight\", \"feathergale-knight-pota\");\nawait moveFile(\"bestiary/humanoid\", \"felbarren-dwarf\", \"felbarren-dwarf-skt\");\nawait moveFile(\"bestiary/humanoid\", \"firefist\", \"firefist-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"first-year-student\", \"first-year-student-scc\");\nawait moveFile(\"bestiary/humanoid\", \"fist-of-bane\", \"fist-of-bane-bgdia\");\nawait moveFile(\"bestiary/humanoid\", \"flamewrath\", \"flamewrath-pota\");\nawait moveFile(\"bestiary/humanoid\", \"flux-blastseeker\", \"flux-blastseeker-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"frontline-medic\", \"frontline-medic-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"frost-druid\", \"frost-druid-idrotf\");\nawait moveFile(\"bestiary/humanoid\", \"galvanic-blastseeker\", \"galvanic-blastseeker-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"giff\", \"giff-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"giff-shipmate\", \"giff-shipmate-bam\");\nawait moveFile(\"bestiary/humanoid\", \"giff-shock-trooper\", \"giff-shock-trooper-bam\");\nawait moveFile(\"bestiary/humanoid\", \"giff-warlord\", \"giff-warlord-bam\");\nawait moveFile(\"bestiary/humanoid\", \"githyanki-buccaneer\", \"githyanki-buccaneer-bam\");\nawait moveFile(\"bestiary/humanoid\", \"githyanki-gish\", \"githyanki-gish-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"githyanki-kithrak\", \"githyanki-kithrak-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"githyanki-star-seer\", \"githyanki-star-seer-bam\");\nawait moveFile(\"bestiary/humanoid\", \"githyanki-supreme-commander\", \"githyanki-supreme-commander-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"githyanki-xenomancer\", \"githyanki-xenomancer-bam\");\nawait moveFile(\"bestiary/humanoid\", \"githzerai-anarch\", \"githzerai-anarch-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"githzerai-enlightened\", \"githzerai-enlightened-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"goblin-commoner\", \"goblin-commoner-tftyp\");\nawait moveFile(\"bestiary/humanoid\", \"goblin-gang-member\", \"goblin-gang-member-kkw\");\nawait moveFile(\"bestiary/humanoid\", \"golgari-shaman\", \"golgari-shaman-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"goliath-warrior\", \"goliath-warrior-idrotf\");\nawait moveFile(\"bestiary/humanoid\", \"goliath-werebear\", \"goliath-werebear-idrotf\");\nawait moveFile(\"bestiary/humanoid\", \"goon\", \"goon-nrh-ass\");\nawait moveFile(\"bestiary/humanoid\", \"gorzils-gang-troglodyte\", \"gorzils-gang-troglodyte-wdmm\");\nawait moveFile(\"bestiary/humanoid\", \"griffon-cavalry-rider\", \"griffon-cavalry-rider-wdh\");\nawait moveFile(\"bestiary/humanoid\", \"grippli-warrior\", \"grippli-warrior-cm\");\nawait moveFile(\"bestiary/humanoid\", \"grung-elite-warrior\", \"grung-elite-warrior-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"grung\", \"grung-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"grung-wildling\", \"grung-wildling-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"hadozee-explorer\", \"hadozee-explorer-bam\");\nawait moveFile(\"bestiary/humanoid\", \"hadozee-shipmate\", \"hadozee-shipmate-bam\");\nawait moveFile(\"bestiary/humanoid\", \"hadozee-warrior\", \"hadozee-warrior-bam\");\nawait moveFile(\"bestiary/humanoid\", \"half-blue-dragon-gladiator\", \"half-blue-dragon-gladiator-rot\");\nawait moveFile(\"bestiary/humanoid\", \"half-green-dragon-assassin\", \"half-green-dragon-assassin-rot\");\nawait moveFile(\"bestiary/humanoid\", \"half-red-dragon-gladiator\", \"half-red-dragon-gladiator-rot\");\nawait moveFile(\"bestiary/humanoid\", \"harengon-brigand\", \"harengon-brigand-wbtw\");\nawait moveFile(\"bestiary/humanoid\", \"harengon-sniper\", \"harengon-sniper-wbtw\");\nawait moveFile(\"bestiary/humanoid\", \"horncaller\", \"horncaller-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"howling-hatred-initiate\", \"howling-hatred-initiate-pota\");\nawait moveFile(\"bestiary/humanoid\", \"howling-hatred-priest\", \"howling-hatred-priest-pota\");\nawait moveFile(\"bestiary/humanoid\", \"hurricane\", \"hurricane-pota\");\nawait moveFile(\"bestiary/humanoid\", \"hybrid-brute\", \"hybrid-brute-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"hybrid-flier\", \"hybrid-flier-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"hybrid-poisoner\", \"hybrid-poisoner-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"hybrid-shocker\", \"hybrid-shocker-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"hybrid-spy\", \"hybrid-spy-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"icewind-kobold\", \"icewind-kobold-idrotf\");\nawait moveFile(\"bestiary/humanoid\", \"illusionist-wizard\", \"illusionist-wizard-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"immortal-lotus-monk\", \"immortal-lotus-monk-cm\");\nawait moveFile(\"bestiary/humanoid\", \"inquisitor-of-the-mind-fire\", \"inquisitor-of-the-mind-fire-vrgr\");\nawait moveFile(\"bestiary/humanoid\", \"inquisitor-of-the-sword\", \"inquisitor-of-the-sword-vrgr\");\nawait moveFile(\"bestiary/humanoid\", \"inquisitor-of-the-tome\", \"inquisitor-of-the-tome-vrgr\");\nawait moveFile(\"bestiary/humanoid\", \"inspired\", \"inspired-erlw\");\nawait moveFile(\"bestiary/humanoid\", \"iron-consul\", \"iron-consul-bgdia\");\nawait moveFile(\"bestiary/humanoid\", \"jermlaine\", \"jermlaine-mff\");\nawait moveFile(\"bestiary/humanoid\", \"junior-drow-priestess-of-lolth\", \"junior-drow-priestess-of-lolth-wdmm\");\nawait moveFile(\"bestiary/humanoid\", \"kalashtar\", \"kalashtar-erlw\");\nawait moveFile(\"bestiary/humanoid\", \"keeper-of-the-feather\", \"keeper-of-the-feather-psi\");\nawait moveFile(\"bestiary/humanoid\", \"king-robbit-the-slimy\", \"king-robbit-the-slimy-mgelft\");\nawait moveFile(\"bestiary/humanoid\", \"knight-of-the-black-sword\", \"knight-of-the-black-sword-idrotf\");\nawait moveFile(\"bestiary/humanoid\", \"knight-of-the-mithral-shield\", \"knight-of-the-mithral-shield-skt\");\nawait moveFile(\"bestiary/humanoid\", \"koalinth\", \"koalinth-gos\");\nawait moveFile(\"bestiary/humanoid\", \"koalinth-sergeant\", \"koalinth-sergeant-gos\");\nawait moveFile(\"bestiary/humanoid\", \"kobold-commoner\", \"kobold-commoner-tftyp\");\nawait moveFile(\"bestiary/humanoid\", \"kobold-elite\", \"kobold-elite-tftyp\");\nawait moveFile(\"bestiary/humanoid\", \"kobold-inventor\", \"kobold-inventor-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"kobold-scale-sorcerer\", \"kobold-scale-sorcerer-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"kobold-tinkerer\", \"kobold-tinkerer-dosi\");\nawait moveFile(\"bestiary/humanoid\", \"kobold-underling\", \"kobold-underling-egw\");\nawait moveFile(\"bestiary/humanoid\", \"kraul-death-priest\", \"kraul-death-priest-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"kraul-warrior\", \"kraul-warrior-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"lava-child\", \"lava-child-wdmm\");\nawait moveFile(\"bestiary/humanoid\", \"lawmage\", \"lawmage-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"leonin-iconoclast\", \"leonin-iconoclast-mot\");\nawait moveFile(\"bestiary/humanoid\", \"lizardfolk-commoner\", \"lizardfolk-commoner-gos\");\nawait moveFile(\"bestiary/humanoid\", \"lizardfolk-render\", \"lizardfolk-render-gos\");\nawait moveFile(\"bestiary/humanoid\", \"lizardfolk-scaleshield\", \"lizardfolk-scaleshield-gos\");\nawait moveFile(\"bestiary/humanoid\", \"lizardfolk-subchief\", \"lizardfolk-subchief-gos\");\nawait moveFile(\"bestiary/humanoid\", \"locathah\", \"locathah-gos\");\nawait moveFile(\"bestiary/humanoid\", \"locathah-hunter\", \"locathah-hunter-gos\");\nawait moveFile(\"bestiary/humanoid\", \"lords-alliance-guard\", \"lords-alliance-guard-oota\");\nawait moveFile(\"bestiary/humanoid\", \"lords-alliance-spy\", \"lords-alliance-spy-oota\");\nawait moveFile(\"bestiary/humanoid\", \"lorehold-apprentice\", \"lorehold-apprentice-scc\");\nawait moveFile(\"bestiary/humanoid\", \"lorehold-pledgemage\", \"lorehold-pledgemage-scc\");\nawait moveFile(\"bestiary/humanoid\", \"lorehold-professor-of-chaos\", \"lorehold-professor-of-chaos-scc\");\nawait moveFile(\"bestiary/humanoid\", \"lorehold-professor-of-order\", \"lorehold-professor-of-order-scc\");\nawait moveFile(\"bestiary/humanoid\", \"magewright\", \"magewright-erlw\");\nawait moveFile(\"bestiary/humanoid\", \"martial-arts-adept\", \"martial-arts-adept-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"master-of-souls\", \"master-of-souls-bgdia\");\nawait moveFile(\"bestiary/humanoid\", \"master-sage\", \"master-sage-cm\");\nawait moveFile(\"bestiary/humanoid\", \"master-thief\", \"master-thief-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"meeseeks\", \"meeseeks-rmbre\");\nawait moveFile(\"bestiary/humanoid\", \"meletian-hoplite\", \"meletian-hoplite-mot\");\nawait moveFile(\"bestiary/humanoid\", \"mercenary-envoy\", \"mercenary-envoy-aitfr-fcd\");\nawait moveFile(\"bestiary/humanoid\", \"merfolk-salvager\", \"merfolk-salvager-gos\");\nawait moveFile(\"bestiary/humanoid\", \"merfolk-scout\", \"merfolk-scout-gos\");\nawait moveFile(\"bestiary/humanoid\", \"mind-mage\", \"mind-mage-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"monastery-of-the-distressed-body-monk\", \"monastery-of-the-distressed-body-monk-llk\");\nawait moveFile(\"bestiary/humanoid\", \"monastic-high-curator\", \"monastic-high-curator-crcotn\");\nawait moveFile(\"bestiary/humanoid\", \"monastic-infiltrator\", \"monastic-infiltrator-crcotn\");\nawait moveFile(\"bestiary/humanoid\", \"monastic-operative\", \"monastic-operative-crcotn\");\nawait moveFile(\"bestiary/humanoid\", \"mongrelfolk\", \"mongrelfolk-cos\");\nawait moveFile(\"bestiary/humanoid\", \"necromancer-wizard\", \"necromancer-wizard-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"necromite-of-myrkul\", \"necromite-of-myrkul-bgdia\");\nawait moveFile(\"bestiary/humanoid\", \"night-blade\", \"night-blade-bgdia\");\nawait moveFile(\"bestiary/humanoid\", \"nightsea-chil-liren\", \"nightsea-chil-liren-jttrc\");\nawait moveFile(\"bestiary/humanoid\", \"norker\", \"norker-mff\");\nawait moveFile(\"bestiary/humanoid\", \"norker-war-leader\", \"norker-war-leader-mff\");\nawait moveFile(\"bestiary/humanoid\", \"occult-extollant\", \"occult-extollant-crcotn\");\nawait moveFile(\"bestiary/humanoid\", \"occult-initiate\", \"occult-initiate-crcotn\");\nawait moveFile(\"bestiary/humanoid\", \"occult-silvertongue\", \"occult-silvertongue-crcotn\");\nawait moveFile(\"bestiary/humanoid\", \"old-troglodyte\", \"old-troglodyte-wdmm\");\nawait moveFile(\"bestiary/humanoid\", \"one-eyed-shiver\", \"one-eyed-shiver-pota\");\nawait moveFile(\"bestiary/humanoid\", \"oracle\", \"oracle-mot\");\nawait moveFile(\"bestiary/humanoid\", \"oracle-of-strixhaven\", \"oracle-of-strixhaven-scc\");\nawait moveFile(\"bestiary/humanoid\", \"orc-blade-of-ilneval\", \"orc-blade-of-ilneval-vgm\");\nawait moveFile(\"bestiary/humanoid\", \"orc-claw-of-luthic\", \"orc-claw-of-luthic-vgm\");\nawait moveFile(\"bestiary/humanoid\", \"orc-commoner\", \"orc-commoner-tftyp\");\nawait moveFile(\"bestiary/humanoid\", \"orc-hand-of-yurtrus\", \"orc-hand-of-yurtrus-vgm\");\nawait moveFile(\"bestiary/humanoid\", \"orc-nurtured-one-of-yurtrus\", \"orc-nurtured-one-of-yurtrus-vgm\");\nawait moveFile(\"bestiary/humanoid\", \"orc-red-fang-of-shargaas\", \"orc-red-fang-of-shargaas-vgm\");\nawait moveFile(\"bestiary/humanoid\", \"oriq-blood-mage\", \"oriq-blood-mage-scc\");\nawait moveFile(\"bestiary/humanoid\", \"oriq-recruiter\", \"oriq-recruiter-scc\");\nawait moveFile(\"bestiary/humanoid\", \"panopticus-wizard\", \"panopticus-wizard-wdh\");\nawait moveFile(\"bestiary/humanoid\", \"pirate-bosun\", \"pirate-bosun-gos\");\nawait moveFile(\"bestiary/humanoid\", \"pirate-captain\", \"pirate-captain-gos\");\nawait moveFile(\"bestiary/humanoid\", \"pirate-deck-wizard\", \"pirate-deck-wizard-gos\");\nawait moveFile(\"bestiary/humanoid\", \"pirate-first-mate\", \"pirate-first-mate-gos\");\nawait moveFile(\"bestiary/humanoid\", \"precognitive-mage\", \"precognitive-mage-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"priest-of-osybus\", \"priest-of-osybus-vrgr\");\nawait moveFile(\"bestiary/humanoid\", \"prismari-apprentice\", \"prismari-apprentice-scc\");\nawait moveFile(\"bestiary/humanoid\", \"prismari-pledgemage\", \"prismari-pledgemage-scc\");\nawait moveFile(\"bestiary/humanoid\", \"prismari-professor-of-expression\", \"prismari-professor-of-expression-scc\");\nawait moveFile(\"bestiary/humanoid\", \"prismari-professor-of-perfection\", \"prismari-professor-of-perfection-scc\");\nawait moveFile(\"bestiary/humanoid\", \"prophetess-dran\", \"prophetess-dran-ai\");\nawait moveFile(\"bestiary/humanoid\", \"quandrix-apprentice\", \"quandrix-apprentice-scc\");\nawait moveFile(\"bestiary/humanoid\", \"quandrix-pledgemage\", \"quandrix-pledgemage-scc\");\nawait moveFile(\"bestiary/humanoid\", \"quandrix-professor-of-substance\", \"quandrix-professor-of-substance-scc\");\nawait moveFile(\"bestiary/humanoid\", \"quandrix-professor-of-theory\", \"quandrix-professor-of-theory-scc\");\nawait moveFile(\"bestiary/humanoid\", \"rakdos-lampooner\", \"rakdos-lampooner-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"rakdos-performer-blade-juggler\", \"rakdos-performer-blade-juggler-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"rakdos-performer-fire-eater\", \"rakdos-performer-fire-eater-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"rakdos-performer-high-wire-acrobat\", \"rakdos-performer-high-wire-acrobat-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"razerblast\", \"razerblast-pota\");\nawait moveFile(\"bestiary/humanoid\", \"reaper-of-bhaal\", \"reaper-of-bhaal-bgdia\");\nawait moveFile(\"bestiary/humanoid\", \"reckoner\", \"reckoner-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"red-wizard\", \"red-wizard-rot\");\nawait moveFile(\"bestiary/humanoid\", \"redbrand-ruffian\", \"redbrand-ruffian-lmop\");\nawait moveFile(\"bestiary/humanoid\", \"reghed-chieftain\", \"reghed-chieftain-idrotf\");\nawait moveFile(\"bestiary/humanoid\", \"reghed-great-warrior\", \"reghed-great-warrior-idrotf\");\nawait moveFile(\"bestiary/humanoid\", \"reghed-shaman\", \"reghed-shaman-idrotf\");\nawait moveFile(\"bestiary/humanoid\", \"reghed-warrior\", \"reghed-warrior-idrotf\");\nawait moveFile(\"bestiary/humanoid\", \"rip-tide-priest\", \"rip-tide-priest-gos\");\nawait moveFile(\"bestiary/humanoid\", \"rock-gnome-recluse\", \"rock-gnome-recluse-dip\");\nawait moveFile(\"bestiary/humanoid\", \"rubblebelt-stalker\", \"rubblebelt-stalker-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"sacred-stone-monk\", \"sacred-stone-monk-pota\");\nawait moveFile(\"bestiary/humanoid\", \"sage\", \"sage-cm\");\nawait moveFile(\"bestiary/humanoid\", \"sahuagin-blademaster\", \"sahuagin-blademaster-gos\");\nawait moveFile(\"bestiary/humanoid\", \"sahuagin-champion\", \"sahuagin-champion-gos\");\nawait moveFile(\"bestiary/humanoid\", \"sahuagin-coral-smasher\", \"sahuagin-coral-smasher-gos\");\nawait moveFile(\"bestiary/humanoid\", \"sahuagin-deep-diver\", \"sahuagin-deep-diver-gos\");\nawait moveFile(\"bestiary/humanoid\", \"sahuagin-high-priestess\", \"sahuagin-high-priestess-gos\");\nawait moveFile(\"bestiary/humanoid\", \"sahuagin-warlock-of-ukotoa\", \"sahuagin-warlock-of-ukotoa-egw\");\nawait moveFile(\"bestiary/humanoid\", \"sahuagin-wave-shaper\", \"sahuagin-wave-shaper-gos\");\nawait moveFile(\"bestiary/humanoid\", \"scholarly-agent\", \"scholarly-agent-crcotn\");\nawait moveFile(\"bestiary/humanoid\", \"scholarly-excavator\", \"scholarly-excavator-crcotn\");\nawait moveFile(\"bestiary/humanoid\", \"scholarly-mastermind\", \"scholarly-mastermind-crcotn\");\nawait moveFile(\"bestiary/humanoid\", \"scorchbringer-guard\", \"scorchbringer-guard-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"sea-elf\", \"sea-elf-skt\");\nawait moveFile(\"bestiary/humanoid\", \"sergeant\", \"sergeant-erlw\");\nawait moveFile(\"bestiary/humanoid\", \"setessan-hoplite\", \"setessan-hoplite-mot\");\nawait moveFile(\"bestiary/humanoid\", \"shadar-kai-gloom-weaver\", \"shadar-kai-gloom-weaver-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"shadar-kai-shadow-dancer\", \"shadar-kai-shadow-dancer-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"shadar-kai-soul-monger\", \"shadar-kai-soul-monger-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"shard-shunner\", \"shard-shunner-wdh\");\nawait moveFile(\"bestiary/humanoid\", \"shield-dwarf-guard\", \"shield-dwarf-guard-skt\");\nawait moveFile(\"bestiary/humanoid\", \"shield-dwarf-noble\", \"shield-dwarf-noble-skt\");\nawait moveFile(\"bestiary/humanoid\", \"shifter\", \"shifter-erlw\");\nawait moveFile(\"bestiary/humanoid\", \"silverquill-apprentice\", \"silverquill-apprentice-scc\");\nawait moveFile(\"bestiary/humanoid\", \"silverquill-pledgemage\", \"silverquill-pledgemage-scc\");\nawait moveFile(\"bestiary/humanoid\", \"silverquill-professor-of-radiance\", \"silverquill-professor-of-radiance-scc\");\nawait moveFile(\"bestiary/humanoid\", \"silverquill-professor-of-shadow\", \"silverquill-professor-of-shadow-scc\");\nawait moveFile(\"bestiary/humanoid\", \"simic-merfolk\", \"simic-merfolk-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"skull-lasher-of-myrkul\", \"skull-lasher-of-myrkul-bgdia\");\nawait moveFile(\"bestiary/humanoid\", \"skyweaver\", \"skyweaver-pota\");\nawait moveFile(\"bestiary/humanoid\", \"sneak\", \"sneak-hol\");\nawait moveFile(\"bestiary/humanoid\", \"soldier\", \"soldier-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"spellcaster\", \"spellcaster-esk\");\nawait moveFile(\"bestiary/humanoid\", \"spellcaster-healer\", \"spellcaster-healer-slw\");\nawait moveFile(\"bestiary/humanoid\", \"spellcaster-mage\", \"spellcaster-mage-dc\");\nawait moveFile(\"bestiary/humanoid\", \"squire\", \"squire-hol\");\nawait moveFile(\"bestiary/humanoid\", \"stonemelder\", \"stonemelder-pota\");\nawait moveFile(\"bestiary/humanoid\", \"surrakar\", \"surrakar-psz\");\nawait moveFile(\"bestiary/humanoid\", \"svirfneblin-wererat\", \"svirfneblin-wererat-oota\");\nawait moveFile(\"bestiary/humanoid\", \"swashbuckler\", \"swashbuckler-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"tabaxi-hunter\", \"tabaxi-hunter-toa\");\nawait moveFile(\"bestiary/humanoid\", \"tabaxi-minstrel\", \"tabaxi-minstrel-toa\");\nawait moveFile(\"bestiary/humanoid\", \"tarkanan-assassin\", \"tarkanan-assassin-erlw\");\nawait moveFile(\"bestiary/humanoid\", \"thayan-apprentice\", \"thayan-apprentice-tftyp\");\nawait moveFile(\"bestiary/humanoid\", \"thayan-warrior\", \"thayan-warrior-tftyp\");\nawait moveFile(\"bestiary/humanoid\", \"thought-spy\", \"thought-spy-ggr\");\nawait moveFile(\"bestiary/humanoid\", \"tiefling-muralist\", \"tiefling-muralist-imr\");\nawait moveFile(\"bestiary/humanoid\", \"tortle-druid\", \"tortle-druid-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"tortle\", \"tortle-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"transmuter-wizard\", \"transmuter-wizard-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"triton-master-of-waves\", \"triton-master-of-waves-mot\");\nawait moveFile(\"bestiary/humanoid\", \"triton-shorestalker\", \"triton-shorestalker-mot\");\nawait moveFile(\"bestiary/humanoid\", \"troglodyte-champion-of-laogzed\", \"troglodyte-champion-of-laogzed-oota\");\nawait moveFile(\"bestiary/humanoid\", \"uthgardt-barbarian-leader\", \"uthgardt-barbarian-leader-skt\");\nawait moveFile(\"bestiary/humanoid\", \"uthgardt-shaman\", \"uthgardt-shaman-skt\");\nawait moveFile(\"bestiary/humanoid\", \"veteran-of-the-gauntlet\", \"veteran-of-the-gauntlet-oota\");\nawait moveFile(\"bestiary/humanoid\", \"vistana-assassin\", \"vistana-assassin-cos\");\nawait moveFile(\"bestiary/humanoid\", \"vistana-bandit-captain\", \"vistana-bandit-captain-cos\");\nawait moveFile(\"bestiary/humanoid\", \"vistana-bandit\", \"vistana-bandit-cos\");\nawait moveFile(\"bestiary/humanoid\", \"vistana-commoner\", \"vistana-commoner-cos\");\nawait moveFile(\"bestiary/humanoid\", \"vistana-guard\", \"vistana-guard-cos\");\nawait moveFile(\"bestiary/humanoid\", \"vistana-spy\", \"vistana-spy-cos\");\nawait moveFile(\"bestiary/humanoid\", \"vistana-thug\", \"vistana-thug-cos\");\nawait moveFile(\"bestiary/humanoid\", \"walking-corpse\", \"walking-corpse-cos\");\nawait moveFile(\"bestiary/humanoid\", \"war-priest\", \"war-priest-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"warforged-soldier\", \"warforged-soldier-erlw\");\nawait moveFile(\"bestiary/humanoid\", \"warlock-of-the-archfey\", \"warlock-of-the-archfey-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"warlock-of-the-fiend\", \"warlock-of-the-fiend-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"warlock-of-the-great-old-one\", \"warlock-of-the-great-old-one-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"warlord\", \"warlord-mpmm\");\nawait moveFile(\"bestiary/humanoid\", \"warrior\", \"warrior-esk\");\nawait moveFile(\"bestiary/humanoid\", \"werebat\", \"werebat-wdmm\");\nawait moveFile(\"bestiary/humanoid\", \"werejaguar\", \"werejaguar-tftyp\");\nawait moveFile(\"bestiary/humanoid\", \"wereraven\", \"wereraven-vrgr\");\nawait moveFile(\"bestiary/humanoid\", \"werewolf-krallenhorde\", \"werewolf-krallenhorde-psi\");\nawait moveFile(\"bestiary/humanoid\", \"witchlight-hand-medium\", \"witchlight-hand-medium-wbtw\");\nawait moveFile(\"bestiary/humanoid\", \"witchlight-hand-small\", \"witchlight-hand-small-wbtw\");\nawait moveFile(\"bestiary/humanoid\", \"witherbloom-apprentice\", \"witherbloom-apprentice-scc\");\nawait moveFile(\"bestiary/humanoid\", \"witherbloom-pledgemage\", \"witherbloom-pledgemage-scc\");\nawait moveFile(\"bestiary/humanoid\", \"witherbloom-professor-of-decay\", \"witherbloom-professor-of-decay-scc\");\nawait moveFile(\"bestiary/humanoid\", \"witherbloom-professor-of-growth\", \"witherbloom-professor-of-growth-scc\");\nawait moveFile(\"bestiary/humanoid\", \"wolfwere-alpha\", \"wolfwere-alpha-mabjov\");\nawait moveFile(\"bestiary/humanoid\", \"wolfwere\", \"wolfwere-mabjov\");\nawait moveFile(\"bestiary/humanoid\", \"wood-elf\", \"wood-elf-skt\");\nawait moveFile(\"bestiary/humanoid\", \"wood-elf-wizard\", \"wood-elf-wizard-cm\");\nawait moveFile(\"bestiary/humanoid\", \"xvart-speaker\", \"xvart-speaker-vgm\");\nawait moveFile(\"bestiary/humanoid\", \"young-troglodyte\", \"young-troglodyte-tftyp\");\nawait moveFile(\"bestiary/humanoid\", \"young-wereraven\", \"young-wereraven-cos\");\nawait moveFile(\"bestiary/humanoid\", \"zhent-martial-arts-adept\", \"zhent-martial-arts-adept-wdh\");\nawait moveFile(\"bestiary/humanoid\", \"zhentarim-thug\", \"zhentarim-thug-oota\");\nawait moveFile(\"bestiary/monstrosity\", \"achaierai\", \"achaierai-mabjov\");\nawait moveFile(\"bestiary/monstrosity\", \"adult-kruthik\", \"adult-kruthik-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"aeorian-absorber\", \"aeorian-absorber-egw\");\nawait moveFile(\"bestiary/monstrosity\", \"aeorian-nullifier\", \"aeorian-nullifier-egw\");\nawait moveFile(\"bestiary/monstrosity\", \"aeorian-reverser\", \"aeorian-reverser-egw\");\nawait moveFile(\"bestiary/monstrosity\", \"aldani-lobsterfolk\", \"aldani-lobsterfolk-toa\");\nawait moveFile(\"bestiary/monstrosity\", \"allowak-abominable-yeti\", \"allowak-abominable-yeti-egw\");\nawait moveFile(\"bestiary/monstrosity\", \"allowak-yeti\", \"allowak-yeti-egw\");\nawait moveFile(\"bestiary/monstrosity\", \"amonkhet-hydra\", \"amonkhet-hydra-psa\");\nawait moveFile(\"bestiary/monstrosity\", \"amonkhet-sphinx\", \"amonkhet-sphinx-psa\");\nawait moveFile(\"bestiary/monstrosity\", \"amphisbaena\", \"amphisbaena-gos\");\nawait moveFile(\"bestiary/monstrosity\", \"ancient-deep-crow\", \"ancient-deep-crow-ai\");\nawait moveFile(\"bestiary/monstrosity\", \"angry-sorrowsworn\", \"angry-sorrowsworn-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"animated-tile-chimera\", \"animated-tile-chimera-rot\");\nawait moveFile(\"bestiary/monstrosity\", \"aphemia\", \"aphemia-mot\");\nawait moveFile(\"bestiary/monstrosity\", \"assassin-bug\", \"assassin-bug-mff\");\nawait moveFile(\"bestiary/monstrosity\", \"asteroid-spider\", \"asteroid-spider-mcv1sc\");\nawait moveFile(\"bestiary/monstrosity\", \"astral-dreadnought\", \"astral-dreadnought-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"aurumvorax-den-leader\", \"aurumvorax-den-leader-jttrc\");\nawait moveFile(\"bestiary/monstrosity\", \"aurumvorax\", \"aurumvorax-jttrc\");\nawait moveFile(\"bestiary/monstrosity\", \"banderhobb\", \"banderhobb-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"blood-toll-harpy\", \"blood-toll-harpy-mot\");\nawait moveFile(\"bestiary/monstrosity\", \"bone-whelk\", \"bone-whelk-bgdia\");\nawait moveFile(\"bestiary/monstrosity\", \"broken-king-antigonos\", \"broken-king-antigonos-mot\");\nawait moveFile(\"bestiary/monstrosity\", \"brown-scavver\", \"brown-scavver-bam\");\nawait moveFile(\"bestiary/monstrosity\", \"carrion-stalker\", \"carrion-stalker-vrgr\");\nawait moveFile(\"bestiary/monstrosity\", \"category-1-krasis\", \"category-1-krasis-ggr\");\nawait moveFile(\"bestiary/monstrosity\", \"category-2-krasis\", \"category-2-krasis-ggr\");\nawait moveFile(\"bestiary/monstrosity\", \"category-3-krasis\", \"category-3-krasis-ggr\");\nawait moveFile(\"bestiary/monstrosity\", \"catoblepas\", \"catoblepas-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"caustic-crawler\", \"caustic-crawler-psz\");\nawait moveFile(\"bestiary/monstrosity\", \"cave-fisher\", \"cave-fisher-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"chitine\", \"chitine-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"choldrith\", \"choldrith-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"chupacabra\", \"chupacabra-psx\");\nawait moveFile(\"bestiary/monstrosity\", \"corrupted-avatar-of-lurue\", \"corrupted-avatar-of-lurue-cm\");\nawait moveFile(\"bestiary/monstrosity\", \"crag-cat\", \"crag-cat-skt\");\nawait moveFile(\"bestiary/monstrosity\", \"criosphinx\", \"criosphinx-psa\");\nawait moveFile(\"bestiary/monstrosity\", \"decapus\", \"decapus-ttp\");\nawait moveFile(\"bestiary/monstrosity\", \"deep-crow\", \"deep-crow-ai\");\nawait moveFile(\"bestiary/monstrosity\", \"deep-scion\", \"deep-scion-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"demogorgon\", \"demogorgon-hftt\");\nawait moveFile(\"bestiary/monstrosity\", \"dining-table-mimic\", \"dining-table-mimic-wdh\");\nawait moveFile(\"bestiary/monstrosity\", \"displacer-beast-kitten\", \"displacer-beast-kitten-wbtw\");\nawait moveFile(\"bestiary/monstrosity\", \"dracohydra\", \"dracohydra-ftd\");\nawait moveFile(\"bestiary/monstrosity\", \"draconian-dreadnought\", \"draconian-dreadnought-ftd\");\nawait moveFile(\"bestiary/monstrosity\", \"draconian-foot-soldier\", \"draconian-foot-soldier-ftd\");\nawait moveFile(\"bestiary/monstrosity\", \"draconian-infiltrator\", \"draconian-infiltrator-ftd\");\nawait moveFile(\"bestiary/monstrosity\", \"draconian-mage\", \"draconian-mage-ftd\");\nawait moveFile(\"bestiary/monstrosity\", \"draconian-mastermind\", \"draconian-mastermind-ftd\");\nawait moveFile(\"bestiary/monstrosity\", \"dragonflesh-abomination\", \"dragonflesh-abomination-ftd\");\nawait moveFile(\"bestiary/monstrosity\", \"dragonflesh-grafter\", \"dragonflesh-grafter-ftd\");\nawait moveFile(\"bestiary/monstrosity\", \"dread-doppelganger\", \"dread-doppelganger-mabjov\");\nawait moveFile(\"bestiary/monstrosity\", \"eblis\", \"eblis-toa\");\nawait moveFile(\"bestiary/monstrosity\", \"egg-hunter-adult\", \"egg-hunter-adult-ftd\");\nawait moveFile(\"bestiary/monstrosity\", \"egg-hunter-hatchling\", \"egg-hunter-hatchling-ftd\");\nawait moveFile(\"bestiary/monstrosity\", \"elder-dinosaur-etali-primal-storm\", \"elder-dinosaur-etali-primal-storm-psx\");\nawait moveFile(\"bestiary/monstrosity\", \"elder-dinosaur-ghalta-primal-hunger\", \"elder-dinosaur-ghalta-primal-hunger-psx\");\nawait moveFile(\"bestiary/monstrosity\", \"elder-dinosaur-nezahal-primal-tide\", \"elder-dinosaur-nezahal-primal-tide-psx\");\nawait moveFile(\"bestiary/monstrosity\", \"elder-dinosaur\", \"elder-dinosaur-psx\");\nawait moveFile(\"bestiary/monstrosity\", \"elder-dinosaur-tetzimoc-primal-death\", \"elder-dinosaur-tetzimoc-primal-death-psx\");\nawait moveFile(\"bestiary/monstrosity\", \"elder-dinosaur-zacama-primal-calamity\", \"elder-dinosaur-zacama-primal-calamity-psx\");\nawait moveFile(\"bestiary/monstrosity\", \"elder-dinosaur-zetalpa-primal-dawn\", \"elder-dinosaur-zetalpa-primal-dawn-psx\");\nawait moveFile(\"bestiary/monstrosity\", \"emberhorn-minotaur\", \"emberhorn-minotaur-pota\");\nawait moveFile(\"bestiary/monstrosity\", \"enhanced-medusa\", \"enhanced-medusa-imr\");\nawait moveFile(\"bestiary/monstrosity\", \"female-steeder\", \"female-steeder-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"fleecemane-lion\", \"fleecemane-lion-mot\");\nawait moveFile(\"bestiary/monstrosity\", \"froghemoth\", \"froghemoth-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"frost-worm\", \"frost-worm-egw\");\nawait moveFile(\"bestiary/monstrosity\", \"gem-stalker\", \"gem-stalker-ftd\");\nawait moveFile(\"bestiary/monstrosity\", \"giant-ice-toad\", \"giant-ice-toad-tftyp\");\nawait moveFile(\"bestiary/monstrosity\", \"giant-river-serpent\", \"giant-river-serpent-psa\");\nawait moveFile(\"bestiary/monstrosity\", \"giant-slug\", \"giant-slug-ttp\");\nawait moveFile(\"bestiary/monstrosity\", \"girallon\", \"girallon-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"gloomstalker\", \"gloomstalker-egw\");\nawait moveFile(\"bestiary/monstrosity\", \"gnoll-flesh-gnawer\", \"gnoll-flesh-gnawer-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"gnoll-hunter\", \"gnoll-hunter-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"gomazoa\", \"gomazoa-psz\");\nawait moveFile(\"bestiary/monstrosity\", \"gray-render\", \"gray-render-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"gray-scavver\", \"gray-scavver-bam\");\nawait moveFile(\"bestiary/monstrosity\", \"gremishka\", \"gremishka-vrgr\");\nawait moveFile(\"bestiary/monstrosity\", \"griffin-type-1\", \"griffin-type-1-psz\");\nawait moveFile(\"bestiary/monstrosity\", \"griffin-type-2\", \"griffin-type-2-psz\");\nawait moveFile(\"bestiary/monstrosity\", \"guardian-wolf\", \"guardian-wolf-egw\");\nawait moveFile(\"bestiary/monstrosity\", \"harpy-matriarch\", \"harpy-matriarch-gos\");\nawait moveFile(\"bestiary/monstrosity\", \"hellion\", \"hellion-psz\");\nawait moveFile(\"bestiary/monstrosity\", \"hippocamp\", \"hippocamp-mot\");\nawait moveFile(\"bestiary/monstrosity\", \"hoard-mimic\", \"hoard-mimic-ftd\");\nawait moveFile(\"bestiary/monstrosity\", \"hoard-scarab\", \"hoard-scarab-ftd\");\nawait moveFile(\"bestiary/monstrosity\", \"homarid\", \"homarid-psd\");\nawait moveFile(\"bestiary/monstrosity\", \"horizonback-tortoise\", \"horizonback-tortoise-egw\");\nawait moveFile(\"bestiary/monstrosity\", \"hungry-sorrowsworn\", \"hungry-sorrowsworn-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"ice-piercer\", \"ice-piercer-idrotf\");\nawait moveFile(\"bestiary/monstrosity\", \"ice-toad\", \"ice-toad-rot\");\nawait moveFile(\"bestiary/monstrosity\", \"infant-basilisk\", \"infant-basilisk-oota\");\nawait moveFile(\"bestiary/monstrosity\", \"infant-hook-horror\", \"infant-hook-horror-oota\");\nawait moveFile(\"bestiary/monstrosity\", \"ironscale-hydra\", \"ironscale-hydra-mot\");\nawait moveFile(\"bestiary/monstrosity\", \"juvenile-hook-horror\", \"juvenile-hook-horror-oota\");\nawait moveFile(\"bestiary/monstrosity\", \"juvenile-kraken\", \"juvenile-kraken-gos\");\nawait moveFile(\"bestiary/monstrosity\", \"juvenile-mimic\", \"juvenile-mimic-tce\");\nawait moveFile(\"bestiary/monstrosity\", \"kamadan\", \"kamadan-toa\");\nawait moveFile(\"bestiary/monstrosity\", \"kraken-priest\", \"kraken-priest-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"kruthik-hive-lord\", \"kruthik-hive-lord-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"large-mimic\", \"large-mimic-rmbre\");\nawait moveFile(\"bestiary/monstrosity\", \"leucrotta\", \"leucrotta-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"liondrake\", \"liondrake-ftd\");\nawait moveFile(\"bestiary/monstrosity\", \"lonely-sorrowsworn\", \"lonely-sorrowsworn-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"lost-sorrowsworn\", \"lost-sorrowsworn-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"loup-garou\", \"loup-garou-vrgr\");\nawait moveFile(\"bestiary/monstrosity\", \"mage-hunter\", \"mage-hunter-scc\");\nawait moveFile(\"bestiary/monstrosity\", \"male-steeder\", \"male-steeder-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"malformed-kraken\", \"malformed-kraken-tftyp\");\nawait moveFile(\"bestiary/monstrosity\", \"manticore-heart-piercer\", \"manticore-heart-piercer-psa\");\nawait moveFile(\"bestiary/monstrosity\", \"meazel\", \"meazel-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"megapede\", \"megapede-bam\");\nawait moveFile(\"bestiary/monstrosity\", \"merrow-shallowpriest\", \"merrow-shallowpriest-egw\");\nawait moveFile(\"bestiary/monstrosity\", \"mimic-chair\", \"mimic-chair-cm\");\nawait moveFile(\"bestiary/monstrosity\", \"molten-magma-roper\", \"molten-magma-roper-pota\");\nawait moveFile(\"bestiary/monstrosity\", \"monstrous-peryton\", \"monstrous-peryton-gos\");\nawait moveFile(\"bestiary/monstrosity\", \"nagpa\", \"nagpa-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"necrotic-centipede\", \"necrotic-centipede-bgdia\");\nawait moveFile(\"bestiary/monstrosity\", \"night-scavver\", \"night-scavver-bam\");\nawait moveFile(\"bestiary/monstrosity\", \"nightmare-beast\", \"nightmare-beast-mcv1sc\");\nawait moveFile(\"bestiary/monstrosity\", \"nyx-fleece-ram\", \"nyx-fleece-ram-mot\");\nawait moveFile(\"bestiary/monstrosity\", \"pest-mascot\", \"pest-mascot-scc\");\nawait moveFile(\"bestiary/monstrosity\", \"pterafolk\", \"pterafolk-toa\");\nawait moveFile(\"bestiary/monstrosity\", \"purple-wormling\", \"purple-wormling-skt\");\nawait moveFile(\"bestiary/monstrosity\", \"reduced-threat-basilisk\", \"reduced-threat-basilisk-tftyp\");\nawait moveFile(\"bestiary/monstrosity\", \"reduced-threat-behir\", \"reduced-threat-behir-tftyp\");\nawait moveFile(\"bestiary/monstrosity\", \"reduced-threat-carrion-crawler\", \"reduced-threat-carrion-crawler-tftyp\");\nawait moveFile(\"bestiary/monstrosity\", \"reduced-threat-darkmantle\", \"reduced-threat-darkmantle-tftyp\");\nawait moveFile(\"bestiary/monstrosity\", \"reduced-threat-displacer-beast\", \"reduced-threat-displacer-beast-tftyp\");\nawait moveFile(\"bestiary/monstrosity\", \"reduced-threat-ettercap\", \"reduced-threat-ettercap-tftyp\");\nawait moveFile(\"bestiary/monstrosity\", \"reduced-threat-hook-horror\", \"reduced-threat-hook-horror-tftyp\");\nawait moveFile(\"bestiary/monstrosity\", \"reduced-threat-owlbear\", \"reduced-threat-owlbear-tftyp\");\nawait moveFile(\"bestiary/monstrosity\", \"reduced-threat-peryton\", \"reduced-threat-peryton-tftyp\");\nawait moveFile(\"bestiary/monstrosity\", \"reduced-threat-remorhaz\", \"reduced-threat-remorhaz-tftyp\");\nawait moveFile(\"bestiary/monstrosity\", \"rowboat-mimic\", \"rowboat-mimic-wdmm\");\nawait moveFile(\"bestiary/monstrosity\", \"runed-behir\", \"runed-behir-wdmm\");\nawait moveFile(\"bestiary/monstrosity\", \"sandwurm\", \"sandwurm-psa\");\nawait moveFile(\"bestiary/monstrosity\", \"sea-lion\", \"sea-lion-tftyp\");\nawait moveFile(\"bestiary/monstrosity\", \"sea-spawn\", \"sea-spawn-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"shadow-mastiff-alpha\", \"shadow-mastiff-alpha-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"shadow-mastiff\", \"shadow-mastiff-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"shadow-spirit-3rd-level-spell\", \"shadow-spirit-3rd-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/monstrosity\", \"shadow-spirit-4th-level-spell\", \"shadow-spirit-4th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/monstrosity\", \"shadow-spirit-5th-level-spell\", \"shadow-spirit-5th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/monstrosity\", \"shadow-spirit-6th-level-spell\", \"shadow-spirit-6th-level-spell-tce\");\nawait moveFile(\"bestiary/monstrosity\", \"shadow-spirit-7th-level-spell\", \"shadow-spirit-7th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/monstrosity\", \"shadow-spirit-8th-level-spell\", \"shadow-spirit-8th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/monstrosity\", \"shadow-spirit-9th-level-spell\", \"shadow-spirit-9th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/monstrosity\", \"shapechanged-roper\", \"shapechanged-roper-wdmm\");\nawait moveFile(\"bestiary/monstrosity\", \"shell-shark\", \"shell-shark-gos\");\nawait moveFile(\"bestiary/monstrosity\", \"skulk\", \"skulk-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"skyjek-roc\", \"skyjek-roc-ggr\");\nawait moveFile(\"bestiary/monstrosity\", \"skyswimmer\", \"skyswimmer-ggr\");\nawait moveFile(\"bestiary/monstrosity\", \"snowy-owlbear\", \"snowy-owlbear-idrotf\");\nawait moveFile(\"bestiary/monstrosity\", \"space-hamster\", \"space-hamster-bam\");\nawait moveFile(\"bestiary/monstrosity\", \"sphinx-of-judgment\", \"sphinx-of-judgment-ggr\");\nawait moveFile(\"bestiary/monstrosity\", \"sphinx-type-1\", \"sphinx-type-1-psz\");\nawait moveFile(\"bestiary/monstrosity\", \"sphinx-type-2\", \"sphinx-type-2-psz\");\nawait moveFile(\"bestiary/monstrosity\", \"spitting-mimic\", \"spitting-mimic-idrotf\");\nawait moveFile(\"bestiary/monstrosity\", \"ssurran-defiler\", \"ssurran-defiler-bam\");\nawait moveFile(\"bestiary/monstrosity\", \"ssurran-poisoner\", \"ssurran-poisoner-bam\");\nawait moveFile(\"bestiary/monstrosity\", \"strigoi\", \"strigoi-vrgr\");\nawait moveFile(\"bestiary/monstrosity\", \"su-monster\", \"su-monster-toa\");\nawait moveFile(\"bestiary/monstrosity\", \"sunbird\", \"sunbird-psx\");\nawait moveFile(\"bestiary/monstrosity\", \"swarm-of-gremishkas\", \"swarm-of-gremishkas-vrgr\");\nawait moveFile(\"bestiary/monstrosity\", \"swarm-of-hoard-scarabs\", \"swarm-of-hoard-scarabs-ftd\");\nawait moveFile(\"bestiary/monstrosity\", \"swavain-basilisk\", \"swavain-basilisk-egw\");\nawait moveFile(\"bestiary/monstrosity\", \"telepathic-pentacle\", \"telepathic-pentacle-idrotf\");\nawait moveFile(\"bestiary/monstrosity\", \"theran-chimera\", \"theran-chimera-mot\");\nawait moveFile(\"bestiary/monstrosity\", \"thessalhydra\", \"thessalhydra-hftt\");\nawait moveFile(\"bestiary/monstrosity\", \"thessalkraken\", \"thessalkraken-imr\");\nawait moveFile(\"bestiary/monstrosity\", \"thri-kreen-gladiator\", \"thri-kreen-gladiator-bam\");\nawait moveFile(\"bestiary/monstrosity\", \"thri-kreen-hunter\", \"thri-kreen-hunter-bam\");\nawait moveFile(\"bestiary/monstrosity\", \"thri-kreen-mystic\", \"thri-kreen-mystic-bam\");\nawait moveFile(\"bestiary/monstrosity\", \"timbermaw\", \"timbermaw-psz\");\nawait moveFile(\"bestiary/monstrosity\", \"tlincalli\", \"tlincalli-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"trapper\", \"trapper-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"tressym\", \"tressym-bgdia\");\nawait moveFile(\"bestiary/monstrosity\", \"two-headed-cerberus\", \"two-headed-cerberus-mot\");\nawait moveFile(\"bestiary/monstrosity\", \"two-headed-owlbear\", \"two-headed-owlbear-imr\");\nawait moveFile(\"bestiary/monstrosity\", \"typhon\", \"typhon-mot\");\nawait moveFile(\"bestiary/monstrosity\", \"undercity-medusa\", \"undercity-medusa-ggr\");\nawait moveFile(\"bestiary/monstrosity\", \"underworld-cerberus\", \"underworld-cerberus-mot\");\nawait moveFile(\"bestiary/monstrosity\", \"unspeakable-horror\", \"unspeakable-horror-vrgr\");\nawait moveFile(\"bestiary/monstrosity\", \"void-scavver\", \"void-scavver-bam\");\nawait moveFile(\"bestiary/monstrosity\", \"wretched-sorrowsworn\", \"wretched-sorrowsworn-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"wurm\", \"wurm-psz\");\nawait moveFile(\"bestiary/monstrosity\", \"xill\", \"xill-mff\");\nawait moveFile(\"bestiary/monstrosity\", \"xvart\", \"xvart-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"xvart-warlock-of-raxivort\", \"xvart-warlock-of-raxivort-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"yakfolk-priest\", \"yakfolk-priest-skt\");\nawait moveFile(\"bestiary/monstrosity\", \"yakfolk-warrior\", \"yakfolk-warrior-skt\");\nawait moveFile(\"bestiary/monstrosity\", \"yeti-leader\", \"yeti-leader-tftyp\");\nawait moveFile(\"bestiary/monstrosity\", \"yeti-tyke\", \"yeti-tyke-idrotf\");\nawait moveFile(\"bestiary/monstrosity\", \"young-basilisk\", \"young-basilisk-oota\");\nawait moveFile(\"bestiary/monstrosity\", \"young-griffon-medium\", \"young-griffon-medium-idrotf\");\nawait moveFile(\"bestiary/monstrosity\", \"young-griffon-small\", \"young-griffon-small-idrotf\");\nawait moveFile(\"bestiary/monstrosity\", \"young-griffon-tiny\", \"young-griffon-tiny-idrotf\");\nawait moveFile(\"bestiary/monstrosity\", \"young-hook-horror\", \"young-hook-horror-oota\");\nawait moveFile(\"bestiary/monstrosity\", \"young-kraken\", \"young-kraken-lr\");\nawait moveFile(\"bestiary/monstrosity\", \"young-kruthik\", \"young-kruthik-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"young-purple-worm\", \"young-purple-worm-pota\");\nawait moveFile(\"bestiary/monstrosity\", \"yuan-ti-anathema\", \"yuan-ti-anathema-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"yuan-ti-broodguard\", \"yuan-ti-broodguard-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"yuan-ti-malison-type-4\", \"yuan-ti-malison-type-4-vgm\");\nawait moveFile(\"bestiary/monstrosity\", \"yuan-ti-malison-type-5\", \"yuan-ti-malison-type-5-vgm\");\nawait moveFile(\"bestiary/monstrosity\", \"yuan-ti-mind-whisperer\", \"yuan-ti-mind-whisperer-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"yuan-ti-nightmare-speaker\", \"yuan-ti-nightmare-speaker-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"yuan-ti-pit-master\", \"yuan-ti-pit-master-mpmm\");\nawait moveFile(\"bestiary/monstrosity\", \"yuan-ti-priest\", \"yuan-ti-priest-toa\");\nawait moveFile(\"bestiary/monstrosity\", \"zorbo\", \"zorbo-toa\");\nawait moveFile(\"bestiary/npc\", \"acererak\", \"acererak-toa\");\nawait moveFile(\"bestiary/npc\", \"aerisi-kalinoth\", \"aerisi-kalinoth-pota\");\nawait moveFile(\"bestiary/npc\", \"afsoun-ghorbani\", \"afsoun-ghorbani-jttrc\");\nawait moveFile(\"bestiary/npc\", \"agathe-silverspoon\", \"agathe-silverspoon-crcotn\");\nawait moveFile(\"bestiary/npc\", \"agdon-longscarf\", \"agdon-longscarf-wbtw\");\nawait moveFile(\"bestiary/npc\", \"agony\", \"agony-lox\");\nawait moveFile(\"bestiary/npc\", \"ahmaergo\", \"ahmaergo-wdh\");\nawait moveFile(\"bestiary/npc\", \"alagarthas\", \"alagarthas-wbtw\");\nawait moveFile(\"bestiary/npc\", \"alastrah\", \"alastrah-skt\");\nawait moveFile(\"bestiary/npc\", \"alchaia\", \"alchaia-wdmm\");\nawait moveFile(\"bestiary/npc\", \"aljanor-keenblade\", \"aljanor-keenblade-oota\");\nawait moveFile(\"bestiary/npc\", \"aloysia-telfan\", \"aloysia-telfan-crcotn\");\nawait moveFile(\"bestiary/npc\", \"alyxian-the-absolved\", \"alyxian-the-absolved-crcotn\");\nawait moveFile(\"bestiary/npc\", \"alyxian-the-callous\", \"alyxian-the-callous-crcotn\");\nawait moveFile(\"bestiary/npc\", \"alyxian-the-dispossessed\", \"alyxian-the-dispossessed-crcotn\");\nawait moveFile(\"bestiary/npc\", \"alyxian-the-tormented\", \"alyxian-the-tormented-crcotn\");\nawait moveFile(\"bestiary/npc\", \"amanisha-manivarshi\", \"amanisha-manivarshi-jttrc\");\nawait moveFile(\"bestiary/npc\", \"amarith-coppervein\", \"amarith-coppervein-oota\");\nawait moveFile(\"bestiary/npc\", \"amble\", \"amble-lr\");\nawait moveFile(\"bestiary/npc\", \"ameyali\", \"ameyali-jttrc\");\nawait moveFile(\"bestiary/npc\", \"amidor-the-dandelion\", \"amidor-the-dandelion-wbtw\");\nawait moveFile(\"bestiary/npc\", \"ammalia-cassalanter\", \"ammalia-cassalanter-wdh\");\nawait moveFile(\"bestiary/npc\", \"amrik-vanthampur\", \"amrik-vanthampur-bgdia\");\nawait moveFile(\"bestiary/npc\", \"anastrasya-karelova\", \"anastrasya-karelova-cos\");\nawait moveFile(\"bestiary/npc\", \"andras\", \"andras-imr\");\nawait moveFile(\"bestiary/npc\", \"arabelle\", \"arabelle-cos\");\nawait moveFile(\"bestiary/npc\", \"aradrine-the-owl\", \"aradrine-the-owl-crcotn\");\nawait moveFile(\"bestiary/npc\", \"arasta\", \"arasta-mot\");\nawait moveFile(\"bestiary/npc\", \"archduke-zariel-of-avernus\", \"archduke-zariel-of-avernus-bgdia\");\nawait moveFile(\"bestiary/npc\", \"arcturia\", \"arcturia-wdmm\");\nawait moveFile(\"bestiary/npc\", \"aribeth-de-tylmarande\", \"aribeth-de-tylmarande-mabjov\");\nawait moveFile(\"bestiary/npc\", \"arkhan-the-cruel\", \"arkhan-the-cruel-bgdia\");\nawait moveFile(\"bestiary/npc\", \"arrant-quill\", \"arrant-quill-cm\");\nawait moveFile(\"bestiary/npc\", \"arrigal\", \"arrigal-cos\");\nawait moveFile(\"bestiary/npc\", \"artus-cimber\", \"artus-cimber-toa\");\nawait moveFile(\"bestiary/npc\", \"aruk-thundercaller-thuunlakalaga\", \"aruk-thundercaller-thuunlakalaga-idrotf\");\nawait moveFile(\"bestiary/npc\", \"asha-vandree\", \"asha-vandree-oota\");\nawait moveFile(\"bestiary/npc\", \"ashann\", \"ashann-crcotn\");\nawait moveFile(\"bestiary/npc\", \"asharra\", \"asharra-toa\");\nawait moveFile(\"bestiary/npc\", \"ashdra\", \"ashdra-tftyp\");\nawait moveFile(\"bestiary/npc\", \"ashtyrranthor\", \"ashtyrranthor-wdmm\");\nawait moveFile(\"bestiary/npc\", \"atash\", \"atash-jttrc\");\nawait moveFile(\"bestiary/npc\", \"atiba-pa\", \"atiba-pa-jttrc\");\nawait moveFile(\"bestiary/npc\", \"augrek-brighthelm\", \"augrek-brighthelm-skt\");\nawait moveFile(\"bestiary/npc\", \"aunt-dellie\", \"aunt-dellie-jttrc\");\nawait moveFile(\"bestiary/npc\", \"aurelia\", \"aurelia-ggr\");\nawait moveFile(\"bestiary/npc\", \"auril-first-form\", \"auril-first-form-idrotf\");\nawait moveFile(\"bestiary/npc\", \"auril-second-form\", \"auril-second-form-idrotf\");\nawait moveFile(\"bestiary/npc\", \"auril-third-form\", \"auril-third-form-idrotf\");\nawait moveFile(\"bestiary/npc\", \"aurinax\", \"aurinax-wdh\");\nawait moveFile(\"bestiary/npc\", \"auspicia-dran\", \"auspicia-dran-ai\");\nawait moveFile(\"bestiary/npc\", \"avarice\", \"avarice-idrotf\");\nawait moveFile(\"bestiary/npc\", \"avi\", \"avi-wdh\");\nawait moveFile(\"bestiary/npc\", \"awa\", \"awa-jttrc\");\nawait moveFile(\"bestiary/npc\", \"ayo-jabe-tier-1\", \"ayo-jabe-tier-1-crcotn\");\nawait moveFile(\"bestiary/npc\", \"ayo-jabe-tier-2\", \"ayo-jabe-tier-2-crcotn\");\nawait moveFile(\"bestiary/npc\", \"ayo-jabe-tier-3\", \"ayo-jabe-tier-3-crcotn\");\nawait moveFile(\"bestiary/npc\", \"azaka-stormfang\", \"azaka-stormfang-toa\");\nawait moveFile(\"bestiary/npc\", \"azbara-jos\", \"azbara-jos-hotdq\");\nawait moveFile(\"bestiary/npc\", \"azra-nir\", \"azra-nir-jttrc\");\nawait moveFile(\"bestiary/npc\", \"baalzebul\", \"baalzebul-mabjov\");\nawait moveFile(\"bestiary/npc\", \"baba-lysaga\", \"baba-lysaga-cos\");\nawait moveFile(\"bestiary/npc\", \"baba-lysagas-creeping-hut\", \"baba-lysagas-creeping-hut-cos\");\nawait moveFile(\"bestiary/npc\", \"bael\", \"bael-mpmm\");\nawait moveFile(\"bestiary/npc\", \"bag-of-nails\", \"bag-of-nails-toa\");\nawait moveFile(\"bestiary/npc\", \"bak-mei\", \"bak-mei-cm\");\nawait moveFile(\"bestiary/npc\", \"bandagh\", \"bandagh-tftyp\");\nawait moveFile(\"bestiary/npc\", \"baphomet\", \"baphomet-mpmm\");\nawait moveFile(\"bestiary/npc\", \"barbatos\", \"barbatos-imr\");\nawait moveFile(\"bestiary/npc\", \"barnacle-bess\", \"barnacle-bess-gos\");\nawait moveFile(\"bestiary/npc\", \"barnibus-blastwind\", \"barnibus-blastwind-wdh\");\nawait moveFile(\"bestiary/npc\", \"baron-vargas-vallakovich\", \"baron-vargas-vallakovich-cos\");\nawait moveFile(\"bestiary/npc\", \"bastian-thermandar\", \"bastian-thermandar-pota\");\nawait moveFile(\"bestiary/npc\", \"bavlorna-blightstraw\", \"bavlorna-blightstraw-wbtw\");\nawait moveFile(\"bestiary/npc\", \"bel\", \"bel-bgdia\");\nawait moveFile(\"bestiary/npc\", \"belak-the-outcast\", \"belak-the-outcast-tftyp\");\nawait moveFile(\"bestiary/npc\", \"belashyrra\", \"belashyrra-erlw\");\nawait moveFile(\"bestiary/npc\", \"beldora\", \"beldora-skt\");\nawait moveFile(\"bestiary/npc\", \"beledros-witherbloom\", \"beledros-witherbloom-scc\");\nawait moveFile(\"bestiary/npc\", \"bepis-honeymaker\", \"bepis-honeymaker-wdh\");\nawait moveFile(\"bestiary/npc\", \"berlain-shadowdusk\", \"berlain-shadowdusk-wdmm\");\nawait moveFile(\"bestiary/npc\", \"beucephalus\", \"beucephalus-cos\");\nawait moveFile(\"bestiary/npc\", \"big-momma\", \"big-momma-lox\");\nawait moveFile(\"bestiary/npc\", \"big-water-slurpent\", \"big-water-slurpent-awm\");\nawait moveFile(\"bestiary/npc\", \"billy-beaver\", \"billy-beaver-rmbre\");\nawait moveFile(\"bestiary/npc\", \"birdsquirrel\", \"birdsquirrel-awm\");\nawait moveFile(\"bestiary/npc\", \"bitter-breath\", \"bitter-breath-bgdia\");\nawait moveFile(\"bestiary/npc\", \"bjornhild-solvigsdottir\", \"bjornhild-solvigsdottir-idrotf\");\nawait moveFile(\"bestiary/npc\", \"black-viper\", \"black-viper-wdh\");\nawait moveFile(\"bestiary/npc\", \"blagothkus\", \"blagothkus-hotdq\");\nawait moveFile(\"bestiary/npc\", \"blind-artist\", \"blind-artist-toa\");\nawait moveFile(\"bestiary/npc\", \"blinded-troll\", \"blinded-troll-wdh\");\nawait moveFile(\"bestiary/npc\", \"blurg\", \"blurg-oota\");\nawait moveFile(\"bestiary/npc\", \"bluto-krogarov\", \"bluto-krogarov-cos\");\nawait moveFile(\"bestiary/npc\", \"bodhi-irenicus\", \"bodhi-irenicus-mabjov\");\nawait moveFile(\"bestiary/npc\", \"bolbara\", \"bolbara-egw\");\nawait moveFile(\"bestiary/npc\", \"bonnie\", \"bonnie-wdh\");\nawait moveFile(\"bestiary/npc\", \"borborygmos\", \"borborygmos-ggr\");\nawait moveFile(\"bestiary/npc\", \"borivik-windheim\", \"borivik-windheim-mabjov\");\nawait moveFile(\"bestiary/npc\", \"bosco-daggerhand\", \"bosco-daggerhand-toa\");\nawait moveFile(\"bestiary/npc\", \"braelen-hatherhand\", \"braelen-hatherhand-pota\");\nawait moveFile(\"bestiary/npc\", \"brahma-lutier\", \"brahma-lutier-ai\");\nawait moveFile(\"bestiary/npc\", \"braxow\", \"braxow-skt\");\nawait moveFile(\"bestiary/npc\", \"bray-martikov\", \"bray-martikov-cos\");\nawait moveFile(\"bestiary/npc\", \"brom-martikov\", \"brom-martikov-cos\");\nawait moveFile(\"bestiary/npc\", \"bronzefume\", \"bronzefume-pota\");\nawait moveFile(\"bestiary/npc\", \"brother-broumane\", \"brother-broumane-jttrc\");\nawait moveFile(\"bestiary/npc\", \"buppido\", \"buppido-oota\");\nawait moveFile(\"bestiary/npc\", \"burney-the-barber\", \"burney-the-barber-bgdia\");\nawait moveFile(\"bestiary/npc\", \"buster-the-bear\", \"buster-the-bear-rmbre\");\nawait moveFile(\"bestiary/npc\", \"calcryx\", \"calcryx-tftyp\");\nawait moveFile(\"bestiary/npc\", \"captain-nghathrod\", \"captain-nghathrod-wdmm\");\nawait moveFile(\"bestiary/npc\", \"captain-othelstan\", \"captain-othelstan-hotdq\");\nawait moveFile(\"bestiary/npc\", \"captain-xendros\", \"captain-xendros-gos\");\nawait moveFile(\"bestiary/npc\", \"cassiok-shadowdusk\", \"cassiok-shadowdusk-wdmm\");\nawait moveFile(\"bestiary/npc\", \"cavil-zaltobar\", \"cavil-zaltobar-pota\");\nawait moveFile(\"bestiary/npc\", \"celeste\", \"celeste-crcotn\");\nawait moveFile(\"bestiary/npc\", \"chief-guh\", \"chief-guh-skt\");\nawait moveFile(\"bestiary/npc\", \"chief-kartha-kaya\", \"chief-kartha-kaya-skt\");\nawait moveFile(\"bestiary/npc\", \"chief-nosnra\", \"chief-nosnra-tftyp\");\nawait moveFile(\"bestiary/npc\", \"chukka\", \"chukka-bgdia\");\nawait moveFile(\"bestiary/npc\", \"cinderhild\", \"cinderhild-skt\");\nawait moveFile(\"bestiary/npc\", \"clapperclaw-the-scarecrow\", \"clapperclaw-the-scarecrow-wbtw\");\nawait moveFile(\"bestiary/npc\", \"claugiyliamatar\", \"claugiyliamatar-skt\");\nawait moveFile(\"bestiary/npc\", \"clonk\", \"clonk-bgdia\");\nawait moveFile(\"bestiary/npc\", \"clovin-belview\", \"clovin-belview-cos\");\nawait moveFile(\"bestiary/npc\", \"cog\", \"cog-skt\");\nawait moveFile(\"bestiary/npc\", \"commodore-krux\", \"commodore-krux-lox\");\nawait moveFile(\"bestiary/npc\", \"copper-stormforge\", \"copper-stormforge-wdmm\");\nawait moveFile(\"bestiary/npc\", \"cornelius-watson\", \"cornelius-watson-mabjov\");\nawait moveFile(\"bestiary/npc\", \"corrin-delmaco\", \"corrin-delmaco-erlw\");\nawait moveFile(\"bestiary/npc\", \"count-thullen\", \"count-thullen-skt\");\nawait moveFile(\"bestiary/npc\", \"countess-sansuri\", \"countess-sansuri-skt\");\nawait moveFile(\"bestiary/npc\", \"cressaro\", \"cressaro-skt\");\nawait moveFile(\"bestiary/npc\", \"crokektoeck\", \"crokektoeck-bgdia\");\nawait moveFile(\"bestiary/npc\", \"cryonax\", \"cryonax-mabjov\");\nawait moveFile(\"bestiary/npc\", \"cryovain\", \"cryovain-skt\");\nawait moveFile(\"bestiary/npc\", \"ctenmiir-the-vampire\", \"ctenmiir-the-vampire-llk\");\nawait moveFile(\"bestiary/npc\", \"curran-corvalin\", \"curran-corvalin-tftyp\");\nawait moveFile(\"bestiary/npc\", \"cyrus-belview\", \"cyrus-belview-cos\");\nawait moveFile(\"bestiary/npc\", \"dagdra-deepforge\", \"dagdra-deepforge-oow\");\nawait moveFile(\"bestiary/npc\", \"dagryn\", \"dagryn-mabjov\");\nawait moveFile(\"bestiary/npc\", \"danika-dorakova\", \"danika-dorakova-cos\");\nawait moveFile(\"bestiary/npc\", \"darathra-shendrel\", \"darathra-shendrel-pota\");\nawait moveFile(\"bestiary/npc\", \"darien\", \"darien-mabjov\");\nawait moveFile(\"bestiary/npc\", \"darz-helgar\", \"darz-helgar-skt\");\nawait moveFile(\"bestiary/npc\", \"davian-martikov\", \"davian-martikov-cos\");\nawait moveFile(\"bestiary/npc\", \"davil-starsong\", \"davil-starsong-wdh\");\nawait moveFile(\"bestiary/npc\", \"deepking-horgar-steelshadow-v\", \"deepking-horgar-steelshadow-v-oota\");\nawait moveFile(\"bestiary/npc\", \"deformed-duergar\", \"deformed-duergar-wdmm\");\nawait moveFile(\"bestiary/npc\", \"demogorgon\", \"demogorgon-mpmm\");\nawait moveFile(\"bestiary/npc\", \"dermot-wurder-tier-1\", \"dermot-wurder-tier-1-crcotn\");\nawait moveFile(\"bestiary/npc\", \"dermot-wurder-tier-2\", \"dermot-wurder-tier-2-crcotn\");\nawait moveFile(\"bestiary/npc\", \"dermot-wurder-tier-3\", \"dermot-wurder-tier-3-crcotn\");\nawait moveFile(\"bestiary/npc\", \"deseyna-majarra\", \"deseyna-majarra-pota\");\nawait moveFile(\"bestiary/npc\", \"devil-dog\", \"devil-dog-oow\");\nawait moveFile(\"bestiary/npc\", \"dezmyr-shadowdusk\", \"dezmyr-shadowdusk-wdmm\");\nawait moveFile(\"bestiary/npc\", \"diderius\", \"diderius-rot\");\nawait moveFile(\"bestiary/npc\", \"dirt-under-nails\", \"dirt-under-nails-rtg\");\nawait moveFile(\"bestiary/npc\", \"diva-luma\", \"diva-luma-jttrc\");\nawait moveFile(\"bestiary/npc\", \"djeneba\", \"djeneba-jttrc\");\nawait moveFile(\"bestiary/npc\", \"don-jon-raskin\", \"don-jon-raskin-dip\");\nawait moveFile(\"bestiary/npc\", \"donaar-blitzen\", \"donaar-blitzen-ai\");\nawait moveFile(\"bestiary/npc\", \"donavich\", \"donavich-cos\");\nawait moveFile(\"bestiary/npc\", \"doru\", \"doru-cos\");\nawait moveFile(\"bestiary/npc\", \"dragonbait\", \"dragonbait-toa\");\nawait moveFile(\"bestiary/npc\", \"dragonpriest\", \"dragonpriest-tftyp\");\nawait moveFile(\"bestiary/npc\", \"dralmorrer-borngray\", \"dralmorrer-borngray-hotdq\");\nawait moveFile(\"bestiary/npc\", \"drannin-splithelm\", \"drannin-splithelm-pota\");\nawait moveFile(\"bestiary/npc\", \"drevin\", \"drevin-tftyp\");\nawait moveFile(\"bestiary/npc\", \"drivvin-freth\", \"drivvin-freth-wdmm\");\nawait moveFile(\"bestiary/npc\", \"droki\", \"droki-oota\");\nawait moveFile(\"bestiary/npc\", \"drufi\", \"drufi-toa\");\nawait moveFile(\"bestiary/npc\", \"duchess-brimskarda\", \"duchess-brimskarda-skt\");\nawait moveFile(\"bestiary/npc\", \"duke-thalamra-vanthampur\", \"duke-thalamra-vanthampur-bgdia\");\nawait moveFile(\"bestiary/npc\", \"duke-zalto\", \"duke-zalto-skt\");\nawait moveFile(\"bestiary/npc\", \"dukha-bhatiyali\", \"dukha-bhatiyali-jttrc\");\nawait moveFile(\"bestiary/npc\", \"durnan\", \"durnan-wdh\");\nawait moveFile(\"bestiary/npc\", \"durnn\", \"durnn-tftyp\");\nawait moveFile(\"bestiary/npc\", \"duvessa-shane\", \"duvessa-shane-skt\");\nawait moveFile(\"bestiary/npc\", \"dyrrn\", \"dyrrn-erlw\");\nawait moveFile(\"bestiary/npc\", \"dzaan\", \"dzaan-idrotf\");\nawait moveFile(\"bestiary/npc\", \"dzaans-simulacrum\", \"dzaans-simulacrum-idrotf\");\nawait moveFile(\"bestiary/npc\", \"east-wind\", \"east-wind-llk\");\nawait moveFile(\"bestiary/npc\", \"ebondeath\", \"ebondeath-dc\");\nawait moveFile(\"bestiary/npc\", \"edwin-odesseiron\", \"edwin-odesseiron-mabjov\");\nawait moveFile(\"bestiary/npc\", \"eigerons-ghost\", \"eigerons-ghost-skt\");\nawait moveFile(\"bestiary/npc\", \"eira\", \"eira-tftyp\");\nawait moveFile(\"bestiary/npc\", \"ekene-afa\", \"ekene-afa-toa\");\nawait moveFile(\"bestiary/npc\", \"eku\", \"eku-toa\");\nawait moveFile(\"bestiary/npc\", \"elaina-sartell\", \"elaina-sartell-lox\");\nawait moveFile(\"bestiary/npc\", \"eldeth-feldrun\", \"eldeth-feldrun-oota\");\nawait moveFile(\"bestiary/npc\", \"elise\", \"elise-vrgr\");\nawait moveFile(\"bestiary/npc\", \"elister\", \"elister-skt\");\nawait moveFile(\"bestiary/npc\", \"elizar-dryflagon\", \"elizar-dryflagon-pota\");\nawait moveFile(\"bestiary/npc\", \"elkhorn\", \"elkhorn-wbtw\");\nawait moveFile(\"bestiary/npc\", \"elliach\", \"elliach-bgdia\");\nawait moveFile(\"bestiary/npc\", \"elok-jaharwon\", \"elok-jaharwon-toa\");\nawait moveFile(\"bestiary/npc\", \"elzerina-cassalanter\", \"elzerina-cassalanter-wdh\");\nawait moveFile(\"bestiary/npc\", \"emberosa\", \"emberosa-wdmm\");\nawait moveFile(\"bestiary/npc\", \"embric\", \"embric-wdh\");\nawait moveFile(\"bestiary/npc\", \"emil-toranescu\", \"emil-toranescu-cos\");\nawait moveFile(\"bestiary/npc\", \"emmek-frewn\", \"emmek-frewn-wdh\");\nawait moveFile(\"bestiary/npc\", \"emo\", \"emo-nrh-at\");\nawait moveFile(\"bestiary/npc\", \"emrakul\", \"emrakul-psz\");\nawait moveFile(\"bestiary/npc\", \"endelyn-moongrave\", \"endelyn-moongrave-wbtw\");\nawait moveFile(\"bestiary/npc\", \"envy\", \"envy-wbtw\");\nawait moveFile(\"bestiary/npc\", \"eo-ashmajiir\", \"eo-ashmajiir-mabjov\");\nawait moveFile(\"bestiary/npc\", \"erky-timbers\", \"erky-timbers-tftyp\");\nawait moveFile(\"bestiary/npc\", \"escher\", \"escher-cos\");\nawait moveFile(\"bestiary/npc\", \"estia\", \"estia-tftyp\");\nawait moveFile(\"bestiary/npc\", \"exethanter\", \"exethanter-cos\");\nawait moveFile(\"bestiary/npc\", \"exul\", \"exul-aitfr-isf\");\nawait moveFile(\"bestiary/npc\", \"ezmerelda-davenir\", \"ezmerelda-davenir-cos\");\nawait moveFile(\"bestiary/npc\", \"ezzat\", \"ezzat-wdmm\");\nawait moveFile(\"bestiary/npc\", \"faerl\", \"faerl-cm\");\nawait moveFile(\"bestiary/npc\", \"fala-lefaliir\", \"fala-lefaliir-wdh\");\nawait moveFile(\"bestiary/npc\", \"falcon-the-hunter\", \"falcon-the-hunter-dip\");\nawait moveFile(\"bestiary/npc\", \"faldorn\", \"faldorn-mabjov\");\nawait moveFile(\"bestiary/npc\", \"farmer\", \"farmer-jttrc\");\nawait moveFile(\"bestiary/npc\", \"faroul\", \"faroul-toa\");\nawait moveFile(\"bestiary/npc\", \"fazrian\", \"fazrian-wdmm\");\nawait moveFile(\"bestiary/npc\", \"fel-ardra\", \"fel-ardra-lox\");\nawait moveFile(\"bestiary/npc\", \"felgolos\", \"felgolos-skt\");\nawait moveFile(\"bestiary/npc\", \"felrekt-lafeen\", \"felrekt-lafeen-wdh\");\nawait moveFile(\"bestiary/npc\", \"fennor\", \"fennor-pota\");\nawait moveFile(\"bestiary/npc\", \"fenthaza\", \"fenthaza-toa\");\nawait moveFile(\"bestiary/npc\", \"feonor\", \"feonor-bgdia\");\nawait moveFile(\"bestiary/npc\", \"ferol-sal\", \"ferol-sal-egw\");\nawait moveFile(\"bestiary/npc\", \"fhenimore\", \"fhenimore-lr\");\nawait moveFile(\"bestiary/npc\", \"fidelio\", \"fidelio-wdmm\");\nawait moveFile(\"bestiary/npc\", \"flabbergast\", \"flabbergast-ai\");\nawait moveFile(\"bestiary/npc\", \"flapjack\", \"flapjack-lox\");\nawait moveFile(\"bestiary/npc\", \"flask-of-wine\", \"flask-of-wine-toa\");\nawait moveFile(\"bestiary/npc\", \"flimp-shagglecran\", \"flimp-shagglecran-mabjov\");\nawait moveFile(\"bestiary/npc\", \"floon-blagmaar\", \"floon-blagmaar-wdh\");\nawait moveFile(\"bestiary/npc\", \"foghomer\", \"foghomer-crcotn\");\nawait moveFile(\"bestiary/npc\", \"fraz-urbluu\", \"fraz-urbluu-mpmm\");\nawait moveFile(\"bestiary/npc\", \"frulam-mondath\", \"frulam-mondath-hotdq\");\nawait moveFile(\"bestiary/npc\", \"fyorl\", \"fyorl-idrotf\");\nawait moveFile(\"bestiary/npc\", \"gadof-blinsky\", \"gadof-blinsky-cos\");\nawait moveFile(\"bestiary/npc\", \"galazeth-prismari\", \"galazeth-prismari-scc\");\nawait moveFile(\"bestiary/npc\", \"galeokaerda\", \"galeokaerda-crcotn\");\nawait moveFile(\"bestiary/npc\", \"galsariad-ardyth-tier-1\", \"galsariad-ardyth-tier-1-crcotn\");\nawait moveFile(\"bestiary/npc\", \"galsariad-ardyth-tier-2\", \"galsariad-ardyth-tier-2-crcotn\");\nawait moveFile(\"bestiary/npc\", \"galsariad-ardyth-tier-3\", \"galsariad-ardyth-tier-3-crcotn\");\nawait moveFile(\"bestiary/npc\", \"galvan\", \"galvan-rot\");\nawait moveFile(\"bestiary/npc\", \"gammon-xungoon\", \"gammon-xungoon-jttrc\");\nawait moveFile(\"bestiary/npc\", \"gar-shatterkeel\", \"gar-shatterkeel-lr\");\nawait moveFile(\"bestiary/npc\", \"garra\", \"garra-erlw\");\nawait moveFile(\"bestiary/npc\", \"garret-levistusson\", \"garret-levistusson-llk\");\nawait moveFile(\"bestiary/npc\", \"gash\", \"gash-oota\");\nawait moveFile(\"bestiary/npc\", \"gearbox\", \"gearbox-llk\");\nawait moveFile(\"bestiary/npc\", \"gertruda\", \"gertruda-cos\");\nawait moveFile(\"bestiary/npc\", \"geryon\", \"geryon-mpmm\");\nawait moveFile(\"bestiary/npc\", \"ghald\", \"ghald-pota\");\nawait moveFile(\"bestiary/npc\", \"ghazrim-duloc\", \"ghazrim-duloc-oota\");\nawait moveFile(\"bestiary/npc\", \"ghelryn-foehammer\", \"ghelryn-foehammer-skt\");\nawait moveFile(\"bestiary/npc\", \"gideon-lightward\", \"gideon-lightward-bgdia\");\nawait moveFile(\"bestiary/npc\", \"gildha-duhn\", \"gildha-duhn-oow\");\nawait moveFile(\"bestiary/npc\", \"gishath-suns-avatar\", \"gishath-suns-avatar-psx\");\nawait moveFile(\"bestiary/npc\", \"glabbagool\", \"glabbagool-oota\");\nawait moveFile(\"bestiary/npc\", \"gloam\", \"gloam-wbtw\");\nawait moveFile(\"bestiary/npc\", \"gloine-nathair-nathair\", \"gloine-nathair-nathair-llk\");\nawait moveFile(\"bestiary/npc\", \"glyster\", \"glyster-wdmm\");\nawait moveFile(\"bestiary/npc\", \"gondolo\", \"gondolo-toa\");\nawait moveFile(\"bestiary/npc\", \"gorka-tharn\", \"gorka-tharn-wdmm\");\nawait moveFile(\"bestiary/npc\", \"gorthok-the-thunder-boar\", \"gorthok-the-thunder-boar-dip\");\nawait moveFile(\"bestiary/npc\", \"gorvan-ironheart\", \"gorvan-ironheart-tftyp\");\nawait moveFile(\"bestiary/npc\", \"grabstab\", \"grabstab-toa\");\nawait moveFile(\"bestiary/npc\", \"grandfather-oak\", \"grandfather-oak-imr\");\nawait moveFile(\"bestiary/npc\", \"grandfather-zitembe\", \"grandfather-zitembe-toa\");\nawait moveFile(\"bestiary/npc\", \"grandolpha-muzgardt\", \"grandolpha-muzgardt-idrotf\");\nawait moveFile(\"bestiary/npc\", \"grazilaxx\", \"grazilaxx-oota\");\nawait moveFile(\"bestiary/npc\", \"grazzt\", \"grazzt-mpmm\");\nawait moveFile(\"bestiary/npc\", \"great-chief-halric-bonesnapper\", \"great-chief-halric-bonesnapper-skt\");\nawait moveFile(\"bestiary/npc\", \"great-kroom-purple-worm\", \"great-kroom-purple-worm-awm\");\nawait moveFile(\"bestiary/npc\", \"great-ulfe\", \"great-ulfe-tftyp\");\nawait moveFile(\"bestiary/npc\", \"grenl\", \"grenl-tftyp\");\nawait moveFile(\"bestiary/npc\", \"grimzod-gargenhale\", \"grimzod-gargenhale-lox\");\nawait moveFile(\"bestiary/npc\", \"grinda-garloth\", \"grinda-garloth-wdh\");\nawait moveFile(\"bestiary/npc\", \"grisha\", \"grisha-oota\");\nawait moveFile(\"bestiary/npc\", \"grumink-the-renegade\", \"grumink-the-renegade-pota\");\nawait moveFile(\"bestiary/npc\", \"grumshar\", \"grumshar-wdh\");\nawait moveFile(\"bestiary/npc\", \"grunka\", \"grunka-oow\");\nawait moveFile(\"bestiary/npc\", \"grutha\", \"grutha-tftyp\");\nawait moveFile(\"bestiary/npc\", \"gryz-alakritos\", \"gryz-alakritos-crcotn\");\nawait moveFile(\"bestiary/npc\", \"gundren-rockseeker\", \"gundren-rockseeker-lmop\");\nawait moveFile(\"bestiary/npc\", \"gunvald-halraggson\", \"gunvald-halraggson-idrotf\");\nawait moveFile(\"bestiary/npc\", \"guthash\", \"guthash-tftyp\");\nawait moveFile(\"bestiary/npc\", \"halaster-blackcloak\", \"halaster-blackcloak-wdmm\");\nawait moveFile(\"bestiary/npc\", \"halaster-puppet\", \"halaster-puppet-wdmm\");\nawait moveFile(\"bestiary/npc\", \"hamish-hewland\", \"hamish-hewland-aitfr-dn\");\nawait moveFile(\"bestiary/npc\", \"hanne-hallen\", \"hanne-hallen-oota\");\nawait moveFile(\"bestiary/npc\", \"harkina-hunt\", \"harkina-hunt-bgdia\");\nawait moveFile(\"bestiary/npc\", \"harshnag\", \"harshnag-skt\");\nawait moveFile(\"bestiary/npc\", \"hastain\", \"hastain-lox\");\nawait moveFile(\"bestiary/npc\", \"haungharassk\", \"haungharassk-wdmm\");\nawait moveFile(\"bestiary/npc\", \"hedrun-arnsfirth\", \"hedrun-arnsfirth-tftyp\");\nawait moveFile(\"bestiary/npc\", \"helga-ruvak\", \"helga-ruvak-cos\");\nawait moveFile(\"bestiary/npc\", \"hellenhild\", \"hellenhild-skt\");\nawait moveFile(\"bestiary/npc\", \"hellenrae\", \"hellenrae-pota\");\nawait moveFile(\"bestiary/npc\", \"hengar-aesnvaard\", \"hengar-aesnvaard-idrotf\");\nawait moveFile(\"bestiary/npc\", \"henrik-van-der-voort\", \"henrik-van-der-voort-cos\");\nawait moveFile(\"bestiary/npc\", \"hester-barch\", \"hester-barch-wdh\");\nawait moveFile(\"bestiary/npc\", \"hew-hackinstone\", \"hew-hackinstone-toa\");\nawait moveFile(\"bestiary/npc\", \"hill-giant-blorbo\", \"hill-giant-blorbo-awm\");\nawait moveFile(\"bestiary/npc\", \"hlam\", \"hlam-wdh\");\nawait moveFile(\"bestiary/npc\", \"horned-sister\", \"horned-sister-wdmm\");\nawait moveFile(\"bestiary/npc\", \"hrabbaz\", \"hrabbaz-wdh\");\nawait moveFile(\"bestiary/npc\", \"hulil-lutan\", \"hulil-lutan-egw\");\nawait moveFile(\"bestiary/npc\", \"hutijin\", \"hutijin-mpmm\");\nawait moveFile(\"bestiary/npc\", \"hydia-moonmusk\", \"hydia-moonmusk-skt\");\nawait moveFile(\"bestiary/npc\", \"hythonia\", \"hythonia-mot\");\nawait moveFile(\"bestiary/npc\", \"ifan-talroa\", \"ifan-talroa-toa\");\nawait moveFile(\"bestiary/npc\", \"iggwilv-the-witch-queen\", \"iggwilv-the-witch-queen-wbtw\");\nawait moveFile(\"bestiary/npc\", \"ilvara-mizzrym\", \"ilvara-mizzrym-oota\");\nawait moveFile(\"bestiary/npc\", \"imelda\", \"imelda-nrh-coi\");\nawait moveFile(\"bestiary/npc\", \"imix\", \"imix-pota\");\nawait moveFile(\"bestiary/npc\", \"imoen\", \"imoen-mabjov\");\nawait moveFile(\"bestiary/npc\", \"imperator-uthor\", \"imperator-uthor-skt\");\nawait moveFile(\"bestiary/npc\", \"insight-acuere\", \"insight-acuere-crcotn\");\nawait moveFile(\"bestiary/npc\", \"iona\", \"iona-psz\");\nawait moveFile(\"bestiary/npc\", \"ireena-kolyana\", \"ireena-kolyana-cos\");\nawait moveFile(\"bestiary/npc\", \"irisoth\", \"irisoth-tftyp\");\nawait moveFile(\"bestiary/npc\", \"iron-spider\", \"iron-spider-wdmm\");\nawait moveFile(\"bestiary/npc\", \"irvan-wastewalker-tier-1\", \"irvan-wastewalker-tier-1-crcotn\");\nawait moveFile(\"bestiary/npc\", \"irvan-wastewalker-tier-2\", \"irvan-wastewalker-tier-2-crcotn\");\nawait moveFile(\"bestiary/npc\", \"irvan-wastewalker-tier-3\", \"irvan-wastewalker-tier-3-crcotn\");\nawait moveFile(\"bestiary/npc\", \"isarr-kronenstrom\", \"isarr-kronenstrom-idrotf\");\nawait moveFile(\"bestiary/npc\", \"isendraug\", \"isendraug-skt\");\nawait moveFile(\"bestiary/npc\", \"ishel\", \"ishel-egw\");\nawait moveFile(\"bestiary/npc\", \"iskander\", \"iskander-rot\");\nawait moveFile(\"bestiary/npc\", \"ismark-kolyanovich\", \"ismark-kolyanovich-cos\");\nawait moveFile(\"bestiary/npc\", \"isolde\", \"isolde-vrgr\");\nawait moveFile(\"bestiary/npc\", \"isperia\", \"isperia-ggr\");\nawait moveFile(\"bestiary/npc\", \"istrid-horn\", \"istrid-horn-wdh\");\nawait moveFile(\"bestiary/npc\", \"iymrith\", \"iymrith-skt\");\nawait moveFile(\"bestiary/npc\", \"izek-strazni\", \"izek-strazni-cos\");\nawait moveFile(\"bestiary/npc\", \"jade-statue\", \"jade-statue-jttrc\");\nawait moveFile(\"bestiary/npc\", \"jade-tigress\", \"jade-tigress-cm\");\nawait moveFile(\"bestiary/npc\", \"jaheira\", \"jaheira-mabjov\");\nawait moveFile(\"bestiary/npc\", \"jalester-silvermane\", \"jalester-silvermane-wdh\");\nawait moveFile(\"bestiary/npc\", \"james-cryon\", \"james-cryon-crcotn\");\nawait moveFile(\"bestiary/npc\", \"jamil-aalithiya\", \"jamil-aalithiya-crcotn\");\nawait moveFile(\"bestiary/npc\", \"jamna-gleamsilver\", \"jamna-gleamsilver-hotdq\");\nawait moveFile(\"bestiary/npc\", \"jandar-chergoba\", \"jandar-chergoba-wdh\");\nawait moveFile(\"bestiary/npc\", \"jarad-vod-savo\", \"jarad-vod-savo-ggr\");\nawait moveFile(\"bestiary/npc\", \"jarl-grugnur\", \"jarl-grugnur-tftyp\");\nawait moveFile(\"bestiary/npc\", \"jarl-storvald\", \"jarl-storvald-skt\");\nawait moveFile(\"bestiary/npc\", \"jarlaxle-baenre\", \"jarlaxle-baenre-wdh\");\nawait moveFile(\"bestiary/npc\", \"jarund-elkhardt\", \"jarund-elkhardt-idrotf\");\nawait moveFile(\"bestiary/npc\", \"jasper-dimmerchasm\", \"jasper-dimmerchasm-skt\");\nawait moveFile(\"bestiary/npc\", \"jelayne\", \"jelayne-oow\");\nawait moveFile(\"bestiary/npc\", \"jenks\", \"jenks-wdh\");\nawait moveFile(\"bestiary/npc\", \"jessamine\", \"jessamine-toa\");\nawait moveFile(\"bestiary/npc\", \"jijibisha-manivarshi\", \"jijibisha-manivarshi-jttrc\");\nawait moveFile(\"bestiary/npc\", \"jim-darkmagic\", \"jim-darkmagic-ai\");\nawait moveFile(\"bestiary/npc\", \"jimjar\", \"jimjar-oota\");\nawait moveFile(\"bestiary/npc\", \"jingle-jangle\", \"jingle-jangle-wbtw\");\nawait moveFile(\"bestiary/npc\", \"jobal\", \"jobal-toa\");\nawait moveFile(\"bestiary/npc\", \"jon-irenicus\", \"jon-irenicus-mabjov\");\nawait moveFile(\"bestiary/npc\", \"jot\", \"jot-tftyp\");\nawait moveFile(\"bestiary/npc\", \"juiblex\", \"juiblex-mpmm\");\nawait moveFile(\"bestiary/npc\", \"kaaltar\", \"kaaltar-skt\");\nawait moveFile(\"bestiary/npc\", \"kaarghaz\", \"kaarghaz-tftyp\");\nawait moveFile(\"bestiary/npc\", \"kadroth\", \"kadroth-idrotf\");\nawait moveFile(\"bestiary/npc\", \"kaevja-cynavern\", \"kaevja-cynavern-wdh\");\nawait moveFile(\"bestiary/npc\", \"kagain\", \"kagain-mabjov\");\nawait moveFile(\"bestiary/npc\", \"kala-mabarin\", \"kala-mabarin-jttrc\");\nawait moveFile(\"bestiary/npc\", \"kalain\", \"kalain-wdh\");\nawait moveFile(\"bestiary/npc\", \"kalka-kylla\", \"kalka-kylla-tftyp\");\nawait moveFile(\"bestiary/npc\", \"karkethzerethzerus-the-sable-despoiler\", \"karkethzerethzerus-the-sable-despoiler-egw\");\nawait moveFile(\"bestiary/npc\", \"kasem-aroon\", \"kasem-aroon-jttrc\");\nawait moveFile(\"bestiary/npc\", \"kasimir-velikov\", \"kasimir-velikov-cos\");\nawait moveFile(\"bestiary/npc\", \"kavil\", \"kavil-wdmm\");\nawait moveFile(\"bestiary/npc\", \"kedjou-kamal\", \"kedjou-kamal-jttrc\");\nawait moveFile(\"bestiary/npc\", \"kelek\", \"kelek-wbtw\");\nawait moveFile(\"bestiary/npc\", \"kella-darkhope\", \"kella-darkhope-skt\");\nawait moveFile(\"bestiary/npc\", \"kelson-darktreader\", \"kelson-darktreader-tftyp\");\nawait moveFile(\"bestiary/npc\", \"keresta-delvingstone\", \"keresta-delvingstone-wdmm\");\nawait moveFile(\"bestiary/npc\", \"kettlesteam-the-kenku\", \"kettlesteam-the-kenku-wbtw\");\nawait moveFile(\"bestiary/npc\", \"khalessa-draga\", \"khalessa-draga-oota\");\nawait moveFile(\"bestiary/npc\", \"khaspere-drylund\", \"khaspere-drylund-skt\");\nawait moveFile(\"bestiary/npc\", \"kianna\", \"kianna-jttrc\");\nawait moveFile(\"bestiary/npc\", \"kieren\", \"kieren-tftyp\");\nawait moveFile(\"bestiary/npc\", \"king-hekaton\", \"king-hekaton-skt\");\nawait moveFile(\"bestiary/npc\", \"king-of-feathers\", \"king-of-feathers-toa\");\nawait moveFile(\"bestiary/npc\", \"king-snurre\", \"king-snurre-tftyp\");\nawait moveFile(\"bestiary/npc\", \"kingsport\", \"kingsport-idrotf\");\nawait moveFile(\"bestiary/npc\", \"kinyel-druugiir\", \"kinyel-druugiir-oota\");\nawait moveFile(\"bestiary/npc\", \"kiril-stoyanovich\", \"kiril-stoyanovich-cos\");\nawait moveFile(\"bestiary/npc\", \"kivan\", \"kivan-mabjov\");\nawait moveFile(\"bestiary/npc\", \"klauth\", \"klauth-skt\");\nawait moveFile(\"bestiary/npc\", \"koldaan\", \"koldaan-wdmm\");\nawait moveFile(\"bestiary/npc\", \"koris\", \"koris-crcotn\");\nawait moveFile(\"bestiary/npc\", \"kostchtchie\", \"kostchtchie-bgdia\");\nawait moveFile(\"bestiary/npc\", \"kozilek\", \"kozilek-psz\");\nawait moveFile(\"bestiary/npc\", \"krebbyg-masqilyr\", \"krebbyg-masqilyr-wdh\");\nawait moveFile(\"bestiary/npc\", \"krell-grohlg\", \"krell-grohlg-gos\");\nawait moveFile(\"bestiary/npc\", \"krenko\", \"krenko-kkw\");\nawait moveFile(\"bestiary/npc\", \"krull\", \"krull-bgdia\");\nawait moveFile(\"bestiary/npc\", \"kthriss-drowb\", \"kthriss-drowb-ai\");\nawait moveFile(\"bestiary/npc\", \"ktulah\", \"ktulah-cm\");\nawait moveFile(\"bestiary/npc\", \"kun-ahn-jun\", \"kun-ahn-jun-jttrc\");\nawait moveFile(\"bestiary/npc\", \"kupalue\", \"kupalue-toa\");\nawait moveFile(\"bestiary/npc\", \"kurr\", \"kurr-oota\");\nawait moveFile(\"bestiary/npc\", \"kusa-xungoon\", \"kusa-xungoon-jttrc\");\nawait moveFile(\"bestiary/npc\", \"kwayothe\", \"kwayothe-toa\");\nawait moveFile(\"bestiary/npc\", \"kyrilla-accursed-gorgon\", \"kyrilla-accursed-gorgon-aitfr-dn\");\nawait moveFile(\"bestiary/npc\", \"kysh\", \"kysh-gos\");\nawait moveFile(\"bestiary/npc\", \"lady-dre\", \"lady-dre-jttrc\");\nawait moveFile(\"bestiary/npc\", \"lady-fiona-wachter\", \"lady-fiona-wachter-cos\");\nawait moveFile(\"bestiary/npc\", \"lady-gondafrey\", \"lady-gondafrey-wdh\");\nawait moveFile(\"bestiary/npc\", \"lady-illmarrow\", \"lady-illmarrow-erlw\");\nawait moveFile(\"bestiary/npc\", \"lady-lydia-petrovna\", \"lady-lydia-petrovna-cos\");\nawait moveFile(\"bestiary/npc\", \"laeral-silverhand\", \"laeral-silverhand-wdh\");\nawait moveFile(\"bestiary/npc\", \"lahnis\", \"lahnis-tftyp\");\nawait moveFile(\"bestiary/npc\", \"laiba-nana-rosse\", \"laiba-nana-rosse-wdh\");\nawait moveFile(\"bestiary/npc\", \"laleh-ghorbani\", \"laleh-ghorbani-jttrc\");\nawait moveFile(\"bestiary/npc\", \"lamai-tyenmo\", \"lamai-tyenmo-jttrc\");\nawait moveFile(\"bestiary/npc\", \"langdedrosa-cyanwrath\", \"langdedrosa-cyanwrath-hotdq\");\nawait moveFile(\"bestiary/npc\", \"laskilar\", \"laskilar-toa\");\nawait moveFile(\"bestiary/npc\", \"laurin-ophidas\", \"laurin-ophidas-crcotn\");\nawait moveFile(\"bestiary/npc\", \"layla-the-lizard\", \"layla-the-lizard-rmbre\");\nawait moveFile(\"bestiary/npc\", \"lazav\", \"lazav-ggr\");\nawait moveFile(\"bestiary/npc\", \"leosin-erlanthar\", \"leosin-erlanthar-hotdq\");\nawait moveFile(\"bestiary/npc\", \"lhammaruntosz\", \"lhammaruntosz-sdw\");\nawait moveFile(\"bestiary/npc\", \"liara-portyr\", \"liara-portyr-toa\");\nawait moveFile(\"bestiary/npc\", \"lief-lipsiege\", \"lief-lipsiege-cos\");\nawait moveFile(\"bestiary/npc\", \"lifferlas\", \"lifferlas-skt\");\nawait moveFile(\"bestiary/npc\", \"linan-swift\", \"linan-swift-hotdq\");\nawait moveFile(\"bestiary/npc\", \"linvala\", \"linvala-psz\");\nawait moveFile(\"bestiary/npc\", \"loading-rig\", \"loading-rig-kkw\");\nawait moveFile(\"bestiary/npc\", \"lorthuun\", \"lorthuun-oota\");\nawait moveFile(\"bestiary/npc\", \"losser-mirklav\", \"losser-mirklav-wdh\");\nawait moveFile(\"bestiary/npc\", \"lothar\", \"lothar-mabjov\");\nawait moveFile(\"bestiary/npc\", \"lu-zhong-yin\", \"lu-zhong-yin-jttrc\");\nawait moveFile(\"bestiary/npc\", \"ludmilla-vilisevic\", \"ludmilla-vilisevic-cos\");\nawait moveFile(\"bestiary/npc\", \"lulu\", \"lulu-bgdia\");\nawait moveFile(\"bestiary/npc\", \"lumalia\", \"lumalia-tftyp\");\nawait moveFile(\"bestiary/npc\", \"lungtian\", \"lungtian-jttrc\");\nawait moveFile(\"bestiary/npc\", \"luvash\", \"luvash-cos\");\nawait moveFile(\"bestiary/npc\", \"lynx-creatlach\", \"lynx-creatlach-imr\");\nawait moveFile(\"bestiary/npc\", \"lyzandra-lyzzie-calderos\", \"lyzandra-lyzzie-calderos-pota\");\nawait moveFile(\"bestiary/npc\", \"maccath-the-crimson\", \"maccath-the-crimson-rot\");\nawait moveFile(\"bestiary/npc\", \"mad-maggie\", \"mad-maggie-bgdia\");\nawait moveFile(\"bestiary/npc\", \"mad-mary\", \"mad-mary-cos\");\nawait moveFile(\"bestiary/npc\", \"madam-eva\", \"madam-eva-cos\");\nawait moveFile(\"bestiary/npc\", \"madam-kulp\", \"madam-kulp-jttrc\");\nawait moveFile(\"bestiary/npc\", \"maddgoths-homunculus\", \"maddgoths-homunculus-wdmm\");\nawait moveFile(\"bestiary/npc\", \"maegera-the-dawn-titan\", \"maegera-the-dawn-titan-skt\");\nawait moveFile(\"bestiary/npc\", \"maegla-tarnlar\", \"maegla-tarnlar-pota\");\nawait moveFile(\"bestiary/npc\", \"maggie-keeneyes-tier-1\", \"maggie-keeneyes-tier-1-crcotn\");\nawait moveFile(\"bestiary/npc\", \"maggie-keeneyes-tier-2\", \"maggie-keeneyes-tier-2-crcotn\");\nawait moveFile(\"bestiary/npc\", \"maggie-keeneyes-tier-3\", \"maggie-keeneyes-tier-3-crcotn\");\nawait moveFile(\"bestiary/npc\", \"magister-umbero-zastro\", \"magister-umbero-zastro-wdh\");\nawait moveFile(\"bestiary/npc\", \"magnifico\", \"magnifico-nrh-coi\");\nawait moveFile(\"bestiary/npc\", \"mahadi-the-rakshasa\", \"mahadi-the-rakshasa-bgdia\");\nawait moveFile(\"bestiary/npc\", \"majesto\", \"majesto-cos\");\nawait moveFile(\"bestiary/npc\", \"malivar\", \"malivar-aitfr-isf\");\nawait moveFile(\"bestiary/npc\", \"manafret-cherryport\", \"manafret-cherryport-wdh\");\nawait moveFile(\"bestiary/npc\", \"manshoon-simulacrum\", \"manshoon-simulacrum-wdh\");\nawait moveFile(\"bestiary/npc\", \"manshoon\", \"manshoon-wdh\");\nawait moveFile(\"bestiary/npc\", \"marfulb\", \"marfulb-rot\");\nawait moveFile(\"bestiary/npc\", \"marisa\", \"marisa-crcotn\");\nawait moveFile(\"bestiary/npc\", \"markham-southwell\", \"markham-southwell-skt\");\nawait moveFile(\"bestiary/npc\", \"marlos-urnrayle\", \"marlos-urnrayle-pota\");\nawait moveFile(\"bestiary/npc\", \"marta-moonshadow\", \"marta-moonshadow-wdmm\");\nawait moveFile(\"bestiary/npc\", \"mary-greymalkin\", \"mary-greymalkin-llk\");\nawait moveFile(\"bestiary/npc\", \"marzena-belview\", \"marzena-belview-cos\");\nawait moveFile(\"bestiary/npc\", \"master-refrum\", \"master-refrum-gos\");\nawait moveFile(\"bestiary/npc\", \"mattrim-threestrings-mereg\", \"mattrim-threestrings-mereg-wdh\");\nawait moveFile(\"bestiary/npc\", \"maude\", \"maude-nrh-at\");\nawait moveFile(\"bestiary/npc\", \"maw-of-sekolah\", \"maw-of-sekolah-gos\");\nawait moveFile(\"bestiary/npc\", \"maxeene\", \"maxeene-wdh\");\nawait moveFile(\"bestiary/npc\", \"melannor-fellbranch\", \"melannor-fellbranch-wdh\");\nawait moveFile(\"bestiary/npc\", \"melissara-shadowdusk\", \"melissara-shadowdusk-wdmm\");\nawait moveFile(\"bestiary/npc\", \"meloon-wardragon\", \"meloon-wardragon-wdh\");\nawait moveFile(\"bestiary/npc\", \"mend-nets\", \"mend-nets-rot\");\nawait moveFile(\"bestiary/npc\", \"mennek-ariz\", \"mennek-ariz-tftyp\");\nawait moveFile(\"bestiary/npc\", \"mephistopheles\", \"mephistopheles-mabjov\");\nawait moveFile(\"bestiary/npc\", \"mercion\", \"mercion-wbtw\");\nawait moveFile(\"bestiary/npc\", \"meri\", \"meri-crcotn\");\nawait moveFile(\"bestiary/npc\", \"mev-flintknapper\", \"mev-flintknapper-oota\");\nawait moveFile(\"bestiary/npc\", \"miirym\", \"miirym-cm\");\nawait moveFile(\"bestiary/npc\", \"milivoj\", \"milivoj-cos\");\nawait moveFile(\"bestiary/npc\", \"minsc-and-boo\", \"minsc-and-boo-mabjov\");\nawait moveFile(\"bestiary/npc\", \"miraj-vizann\", \"miraj-vizann-pota\");\nawait moveFile(\"bestiary/npc\", \"miros-xelbrin\", \"miros-xelbrin-skt\");\nawait moveFile(\"bestiary/npc\", \"mirran\", \"mirran-skt\");\nawait moveFile(\"bestiary/npc\", \"mirt\", \"mirt-wdh\");\nawait moveFile(\"bestiary/npc\", \"mishka-belview\", \"mishka-belview-cos\");\nawait moveFile(\"bestiary/npc\", \"mister-light\", \"mister-light-wbtw\");\nawait moveFile(\"bestiary/npc\", \"mister-threadneedle\", \"mister-threadneedle-toa\");\nawait moveFile(\"bestiary/npc\", \"mister-witch\", \"mister-witch-wbtw\");\nawait moveFile(\"bestiary/npc\", \"mjenir\", \"mjenir-idrotf\");\nawait moveFile(\"bestiary/npc\", \"mobar\", \"mobar-wdmm\");\nawait moveFile(\"bestiary/npc\", \"moghadam\", \"moghadam-imr\");\nawait moveFile(\"bestiary/npc\", \"molliver\", \"molliver-wbtw\");\nawait moveFile(\"bestiary/npc\", \"moloch\", \"moloch-mpmm\");\nawait moveFile(\"bestiary/npc\", \"monastery-of-the-distressed-body-grand-master\", \"monastery-of-the-distressed-body-grand-master-llk\");\nawait moveFile(\"bestiary/npc\", \"montaron-and-the-laughing-skull\", \"montaron-and-the-laughing-skull-mabjov\");\nawait moveFile(\"bestiary/npc\", \"morak-urgray\", \"morak-urgray-skt\");\nawait moveFile(\"bestiary/npc\", \"mordakhesh\", \"mordakhesh-erlw\");\nawait moveFile(\"bestiary/npc\", \"morgantha\", \"morgantha-cos\");\nawait moveFile(\"bestiary/npc\", \"morgn\", \"morgn-ai\");\nawait moveFile(\"bestiary/npc\", \"mormesk-the-wraith\", \"mormesk-the-wraith-lmop\");\nawait moveFile(\"bestiary/npc\", \"mortlock-vanthampur\", \"mortlock-vanthampur-bgdia\");\nawait moveFile(\"bestiary/npc\", \"morwena-veilmist\", \"morwena-veilmist-aitfr-thp\");\nawait moveFile(\"bestiary/npc\", \"mossback-steward\", \"mossback-steward-egw\");\nawait moveFile(\"bestiary/npc\", \"mr-dory\", \"mr-dory-gos\");\nawait moveFile(\"bestiary/npc\", \"mr-greystone\", \"mr-greystone-nrh-at\");\nawait moveFile(\"bestiary/npc\", \"mr-honeycutt\", \"mr-honeycutt-nrh-tcmc\");\nawait moveFile(\"bestiary/npc\", \"muiral\", \"muiral-wdmm\");\nawait moveFile(\"bestiary/npc\", \"murgaxor\", \"murgaxor-scc\");\nawait moveFile(\"bestiary/npc\", \"musharib\", \"musharib-toa\");\nawait moveFile(\"bestiary/npc\", \"mwaxanare\", \"mwaxanare-toa\");\nawait moveFile(\"bestiary/npc\", \"myla\", \"myla-dosi\");\nawait moveFile(\"bestiary/npc\", \"myx-nargis-ruba\", \"myx-nargis-ruba-jttrc\");\nawait moveFile(\"bestiary/npc\", \"na\", \"na-toa\");\nawait moveFile(\"bestiary/npc\", \"naergoth-bladelord\", \"naergoth-bladelord-rot\");\nawait moveFile(\"bestiary/npc\", \"naes-inuus\", \"naes-inuus-mabjov\");\nawait moveFile(\"bestiary/npc\", \"nahual\", \"nahual-tftyp\");\nawait moveFile(\"bestiary/npc\", \"nanny-pupu\", \"nanny-pupu-toa\");\nawait moveFile(\"bestiary/npc\", \"narbeck-horn\", \"narbeck-horn-skt\");\nawait moveFile(\"bestiary/npc\", \"narl-xibrindas\", \"narl-xibrindas-wdh\");\nawait moveFile(\"bestiary/npc\", \"narrak\", \"narrak-oota\");\nawait moveFile(\"bestiary/npc\", \"narth-tezrin\", \"narth-tezrin-skt\");\nawait moveFile(\"bestiary/npc\", \"nass-lantomirs-ghost\", \"nass-lantomirs-ghost-idrotf\");\nawait moveFile(\"bestiary/npc\", \"nat\", \"nat-wdh\");\nawait moveFile(\"bestiary/npc\", \"nauk\", \"nauk-mabjov\");\nawait moveFile(\"bestiary/npc\", \"navid\", \"navid-jttrc\");\nawait moveFile(\"bestiary/npc\", \"naxene-drathkala\", \"naxene-drathkala-skt\");\nawait moveFile(\"bestiary/npc\", \"nedylene\", \"nedylene-tftyp\");\nawait moveFile(\"bestiary/npc\", \"nene\", \"nene-jttrc\");\nawait moveFile(\"bestiary/npc\", \"nepartak\", \"nepartak-toa\");\nawait moveFile(\"bestiary/npc\", \"neronvain\", \"neronvain-rot\");\nawait moveFile(\"bestiary/npc\", \"nerozar-the-defeated\", \"nerozar-the-defeated-wdmm\");\nawait moveFile(\"bestiary/npc\", \"nester\", \"nester-wdmm\");\nawait moveFile(\"bestiary/npc\", \"nezznar-the-black-spider\", \"nezznar-the-black-spider-lmop\");\nawait moveFile(\"bestiary/npc\", \"nihiloor\", \"nihiloor-wdh\");\nawait moveFile(\"bestiary/npc\", \"nikolai-wachter\", \"nikolai-wachter-cos\");\nawait moveFile(\"bestiary/npc\", \"niles-breakbone\", \"niles-breakbone-toa\");\nawait moveFile(\"bestiary/npc\", \"nimir\", \"nimir-skt\");\nawait moveFile(\"bestiary/npc\", \"nimira\", \"nimira-tftyp\");\nawait moveFile(\"bestiary/npc\", \"nimuel\", \"nimuel-jttrc\");\nawait moveFile(\"bestiary/npc\", \"nine-fingers-keene\", \"nine-fingers-keene-bgdia\");\nawait moveFile(\"bestiary/npc\", \"nintra-siotta\", \"nintra-siotta-cm\");\nawait moveFile(\"bestiary/npc\", \"niv-mizzet\", \"niv-mizzet-ggr\");\nawait moveFile(\"bestiary/npc\", \"noori\", \"noori-skt\");\nawait moveFile(\"bestiary/npc\", \"noska-urgray\", \"noska-urgray-wdh\");\nawait moveFile(\"bestiary/npc\", \"nundro-rockseeker\", \"nundro-rockseeker-lmop\");\nawait moveFile(\"bestiary/npc\", \"nurvureem-the-dark-lady\", \"nurvureem-the-dark-lady-pota\");\nawait moveFile(\"bestiary/npc\", \"nym\", \"nym-skt\");\nawait moveFile(\"bestiary/npc\", \"oak-truestrike\", \"oak-truestrike-ai\");\nawait moveFile(\"bestiary/npc\", \"obaya-uday\", \"obaya-uday-wdh\");\nawait moveFile(\"bestiary/npc\", \"obliteros\", \"obliteros-wdh\");\nawait moveFile(\"bestiary/npc\", \"obmi\", \"obmi-tftyp\");\nawait moveFile(\"bestiary/npc\", \"oceanus\", \"oceanus-gos\");\nawait moveFile(\"bestiary/npc\", \"ogre-lord-buhfal-ii\", \"ogre-lord-buhfal-ii-egw\");\nawait moveFile(\"bestiary/npc\", \"ogremoch\", \"ogremoch-pota\");\nawait moveFile(\"bestiary/npc\", \"olara\", \"olara-crcotn\");\nawait moveFile(\"bestiary/npc\", \"old-croaker\", \"old-croaker-egw\");\nawait moveFile(\"bestiary/npc\", \"olhydra\", \"olhydra-pota\");\nawait moveFile(\"bestiary/npc\", \"ollin\", \"ollin-jttrc\");\nawait moveFile(\"bestiary/npc\", \"omin-dran\", \"omin-dran-ai\");\nawait moveFile(\"bestiary/npc\", \"ontharr-frume\", \"ontharr-frume-hotdq\");\nawait moveFile(\"bestiary/npc\", \"onyx\", \"onyx-oow\");\nawait moveFile(\"bestiary/npc\", \"oracs-the-enduring\", \"oracs-the-enduring-egw\");\nawait moveFile(\"bestiary/npc\", \"orcus\", \"orcus-mpmm\");\nawait moveFile(\"bestiary/npc\", \"oreioth\", \"oreioth-pota\");\nawait moveFile(\"bestiary/npc\", \"oren-yogilvy\", \"oren-yogilvy-skt\");\nawait moveFile(\"bestiary/npc\", \"orlekto\", \"orlekto-skt\");\nawait moveFile(\"bestiary/npc\", \"orok\", \"orok-skt\");\nawait moveFile(\"bestiary/npc\", \"orond-gralhund\", \"orond-gralhund-wdh\");\nawait moveFile(\"bestiary/npc\", \"ortimay-swift-and-dark\", \"ortimay-swift-and-dark-toa\");\nawait moveFile(\"bestiary/npc\", \"orvex-ocrammas\", \"orvex-ocrammas-toa\");\nawait moveFile(\"bestiary/npc\", \"osvaldo-cassalanter\", \"osvaldo-cassalanter-wdh\");\nawait moveFile(\"bestiary/npc\", \"othokent\", \"othokent-gos\");\nawait moveFile(\"bestiary/npc\", \"othovir\", \"othovir-skt\");\nawait moveFile(\"bestiary/npc\", \"ott-steeltoes\", \"ott-steeltoes-wdh\");\nawait moveFile(\"bestiary/npc\", \"otto-belview\", \"otto-belview-cos\");\nawait moveFile(\"bestiary/npc\", \"otto\", \"otto-wdmm\");\nawait moveFile(\"bestiary/npc\", \"ougalop\", \"ougalop-oota\");\nawait moveFile(\"bestiary/npc\", \"oussa\", \"oussa-tftyp\");\nawait moveFile(\"bestiary/npc\", \"padraich\", \"padraich-pota\");\nawait moveFile(\"bestiary/npc\", \"paloma\", \"paloma-jttrc\");\nawait moveFile(\"bestiary/npc\", \"paolo-maykapal\", \"paolo-maykapal-jttrc\");\nawait moveFile(\"bestiary/npc\", \"parriwimple\", \"parriwimple-cos\");\nawait moveFile(\"bestiary/npc\", \"parson-pellinost\", \"parson-pellinost-egw\");\nawait moveFile(\"bestiary/npc\", \"patrina-velikovna\", \"patrina-velikovna-cos\");\nawait moveFile(\"bestiary/npc\", \"pazuzu\", \"pazuzu-mabjov\");\nawait moveFile(\"bestiary/npc\", \"peebles\", \"peebles-oota\");\nawait moveFile(\"bestiary/npc\", \"pelyious-avhoste\", \"pelyious-avhoste-mabjov\");\nawait moveFile(\"bestiary/npc\", \"pendragon-beestinger\", \"pendragon-beestinger-ai\");\nawait moveFile(\"bestiary/npc\", \"perigee\", \"perigee-crcotn\");\nawait moveFile(\"bestiary/npc\", \"phaia\", \"phaia-tftyp\");\nawait moveFile(\"bestiary/npc\", \"phantom-warrior-archer\", \"phantom-warrior-archer-cos\");\nawait moveFile(\"bestiary/npc\", \"pharblex-spattergoo\", \"pharblex-spattergoo-hotdq\");\nawait moveFile(\"bestiary/npc\", \"phoenix-anvil\", \"phoenix-anvil-ai\");\nawait moveFile(\"bestiary/npc\", \"piccolo\", \"piccolo-cos\");\nawait moveFile(\"bestiary/npc\", \"pidlwick-ii\", \"pidlwick-ii-cos\");\nawait moveFile(\"bestiary/npc\", \"piggy-wiggle-butt\", \"piggy-wiggle-butt-rmbre\");\nawait moveFile(\"bestiary/npc\", \"play-by-play-generator\", \"play-by-play-generator-wdmm\");\nawait moveFile(\"bestiary/npc\", \"pollenella-the-honeybee\", \"pollenella-the-honeybee-wbtw\");\nawait moveFile(\"bestiary/npc\", \"polukranos\", \"polukranos-mot\");\nawait moveFile(\"bestiary/npc\", \"portentia-dran\", \"portentia-dran-ai\");\nawait moveFile(\"bestiary/npc\", \"portia-dzuth\", \"portia-dzuth-wdmm\");\nawait moveFile(\"bestiary/npc\", \"pow-ming\", \"pow-ming-skt\");\nawait moveFile(\"bestiary/npc\", \"preeta-kreepa\", \"preeta-kreepa-wdmm\");\nawait moveFile(\"bestiary/npc\", \"prince-derendil\", \"prince-derendil-oota\");\nawait moveFile(\"bestiary/npc\", \"prince-kirina\", \"prince-kirina-jttrc\");\nawait moveFile(\"bestiary/npc\", \"prince-simbon\", \"prince-simbon-jttrc\");\nawait moveFile(\"bestiary/npc\", \"prince-xeleth\", \"prince-xeleth-lox\");\nawait moveFile(\"bestiary/npc\", \"princeps-kovik\", \"princeps-kovik-bgdia\");\nawait moveFile(\"bestiary/npc\", \"princess-ebonmire\", \"princess-ebonmire-oota\");\nawait moveFile(\"bestiary/npc\", \"princess-serissa\", \"princess-serissa-skt\");\nawait moveFile(\"bestiary/npc\", \"princess-xedalli\", \"princess-xedalli-lox\");\nawait moveFile(\"bestiary/npc\", \"prisoner-237\", \"prisoner-237-idrotf\");\nawait moveFile(\"bestiary/npc\", \"prolix-yusaf\", \"prolix-yusaf-crcotn\");\nawait moveFile(\"bestiary/npc\", \"qawasha\", \"qawasha-toa\");\nawait moveFile(\"bestiary/npc\", \"quenthel-baenre\", \"quenthel-baenre-oota\");\nawait moveFile(\"bestiary/npc\", \"rabbithead\", \"rabbithead-wdmm\");\nawait moveFile(\"bestiary/npc\", \"raegrin-mau\", \"raegrin-mau-egw\");\nawait moveFile(\"bestiary/npc\", \"raezil\", \"raezil-wbtw\");\nawait moveFile(\"bestiary/npc\", \"raggadragga\", \"raggadragga-bgdia\");\nawait moveFile(\"bestiary/npc\", \"raggnar-redtooth\", \"raggnar-redtooth-hotdq\");\nawait moveFile(\"bestiary/npc\", \"rahadin\", \"rahadin-cos\");\nawait moveFile(\"bestiary/npc\", \"rak-tulkhesh\", \"rak-tulkhesh-erlw\");\nawait moveFile(\"bestiary/npc\", \"rakdos\", \"rakdos-ggr\");\nawait moveFile(\"bestiary/npc\", \"ram-sugar\", \"ram-sugar-cm\");\nawait moveFile(\"bestiary/npc\", \"ras-nsi\", \"ras-nsi-toa\");\nawait moveFile(\"bestiary/npc\", \"rath-modar\", \"rath-modar-hotdq\");\nawait moveFile(\"bestiary/npc\", \"remallia-haventree\", \"remallia-haventree-wdh\");\nawait moveFile(\"bestiary/npc\", \"renaer-neverember\", \"renaer-neverember-wdh\");\nawait moveFile(\"bestiary/npc\", \"renwick\", \"renwick-pota\");\nawait moveFile(\"bestiary/npc\", \"rezmir\", \"rezmir-hotdq\");\nawait moveFile(\"bestiary/npc\", \"rhundorth\", \"rhundorth-pota\");\nawait moveFile(\"bestiary/npc\", \"rictavio\", \"rictavio-cos\");\nawait moveFile(\"bestiary/npc\", \"rilsa-rael\", \"rilsa-rael-bgdia\");\nawait moveFile(\"bestiary/npc\", \"ringlerun\", \"ringlerun-wbtw\");\nawait moveFile(\"bestiary/npc\", \"rishaal-the-page-turner\", \"rishaal-the-page-turner-wdh\");\nawait moveFile(\"bestiary/npc\", \"river-mist\", \"river-mist-toa\");\nawait moveFile(\"bestiary/npc\", \"ront\", \"ront-oota\");\nawait moveFile(\"bestiary/npc\", \"rool\", \"rool-skt\");\nawait moveFile(\"bestiary/npc\", \"rosavalda-rose-durst\", \"rosavalda-rose-durst-cos\");\nawait moveFile(\"bestiary/npc\", \"rosie-beestinger\", \"rosie-beestinger-ai\");\nawait moveFile(\"bestiary/npc\", \"rumpadump\", \"rumpadump-oota\");\nawait moveFile(\"bestiary/npc\", \"runara\", \"runara-dosi\");\nawait moveFile(\"bestiary/npc\", \"rystia-zav\", \"rystia-zav-oota\");\nawait moveFile(\"bestiary/npc\", \"saemon-havarian\", \"saemon-havarian-mabjov\");\nawait moveFile(\"bestiary/npc\", \"saeth-cromley\", \"saeth-cromley-wdh\");\nawait moveFile(\"bestiary/npc\", \"saleeth-the-couatl\", \"saleeth-the-couatl-awm\");\nawait moveFile(\"bestiary/npc\", \"salida\", \"salida-toa\");\nawait moveFile(\"bestiary/npc\", \"samara-strongbones\", \"samara-strongbones-wdh\");\nawait moveFile(\"bestiary/npc\", \"samira-arah\", \"samira-arah-jttrc\");\nawait moveFile(\"bestiary/npc\", \"sanbalet\", \"sanbalet-gos\");\nawait moveFile(\"bestiary/npc\", \"sandesyl-morgia\", \"sandesyl-morgia-hotdq\");\nawait moveFile(\"bestiary/npc\", \"sangzor\", \"sangzor-cos\");\nawait moveFile(\"bestiary/npc\", \"sarevok\", \"sarevok-mabjov\");\nawait moveFile(\"bestiary/npc\", \"sarith-kzekarit\", \"sarith-kzekarit-oota\");\nawait moveFile(\"bestiary/npc\", \"sauriv\", \"sauriv-gos\");\nawait moveFile(\"bestiary/npc\", \"savid\", \"savid-cos\");\nawait moveFile(\"bestiary/npc\", \"scribble\", \"scribble-crcotn\");\nawait moveFile(\"bestiary/npc\", \"sekelok\", \"sekelok-toa\");\nawait moveFile(\"bestiary/npc\", \"selenelion-twin\", \"selenelion-twin-wbtw\");\nawait moveFile(\"bestiary/npc\", \"seodra\", \"seodra-imr\");\nawait moveFile(\"bestiary/npc\", \"sephek-kaltro\", \"sephek-kaltro-idrotf\");\nawait moveFile(\"bestiary/npc\", \"serapio\", \"serapio-jttrc\");\nawait moveFile(\"bestiary/npc\", \"severin\", \"severin-rot\");\nawait moveFile(\"bestiary/npc\", \"shadrix-silverquill\", \"shadrix-silverquill-scc\");\nawait moveFile(\"bestiary/npc\", \"shago\", \"shago-toa\");\nawait moveFile(\"bestiary/npc\", \"shaldoor\", \"shaldoor-skt\");\nawait moveFile(\"bestiary/npc\", \"shalendra-floshin\", \"shalendra-floshin-tftyp\");\nawait moveFile(\"bestiary/npc\", \"shalvus-martholio\", \"shalvus-martholio-skt\");\nawait moveFile(\"bestiary/npc\", \"sharda\", \"sharda-skt\");\nawait moveFile(\"bestiary/npc\", \"sharwyn-hucrele\", \"sharwyn-hucrele-tftyp\");\nawait moveFile(\"bestiary/npc\", \"shedrak-of-the-eyes\", \"shedrak-of-the-eyes-oota\");\nawait moveFile(\"bestiary/npc\", \"shemshime\", \"shemshime-cm\");\nawait moveFile(\"bestiary/npc\", \"shira\", \"shira-crcotn\");\nawait moveFile(\"bestiary/npc\", \"shoalar-quanderil\", \"shoalar-quanderil-lr\");\nawait moveFile(\"bestiary/npc\", \"shockerstomper\", \"shockerstomper-wdmm\");\nawait moveFile(\"bestiary/npc\", \"sholeh\", \"sholeh-jttrc\");\nawait moveFile(\"bestiary/npc\", \"shunn-shurreth\", \"shunn-shurreth-wdmm\");\nawait moveFile(\"bestiary/npc\", \"shuushar-the-awakened\", \"shuushar-the-awakened-oota\");\nawait moveFile(\"bestiary/npc\", \"sildar-hallwinter\", \"sildar-hallwinter-lmop\");\nawait moveFile(\"bestiary/npc\", \"sinensa\", \"sinensa-dosi\");\nawait moveFile(\"bestiary/npc\", \"sing-along\", \"sing-along-wdmm\");\nawait moveFile(\"bestiary/npc\", \"sir-baric-nylef\", \"sir-baric-nylef-skt\");\nawait moveFile(\"bestiary/npc\", \"sir-braford\", \"sir-braford-tftyp\");\nawait moveFile(\"bestiary/npc\", \"sir-godfrey-gwilym\", \"sir-godfrey-gwilym-cos\");\nawait moveFile(\"bestiary/npc\", \"sir-talavar\", \"sir-talavar-wbtw\");\nawait moveFile(\"bestiary/npc\", \"sir-ursas\", \"sir-ursas-imr\");\nawait moveFile(\"bestiary/npc\", \"sirac-of-suzail\", \"sirac-of-suzail-skt\");\nawait moveFile(\"bestiary/npc\", \"siren\", \"siren-tftyp\");\nawait moveFile(\"bestiary/npc\", \"skabatha-nightshade\", \"skabatha-nightshade-wbtw\");\nawait moveFile(\"bestiary/npc\", \"skeemo-weirdbottle\", \"skeemo-weirdbottle-wdh\");\nawait moveFile(\"bestiary/npc\", \"sken-zabriss\", \"sken-zabriss-egw\");\nawait moveFile(\"bestiary/npc\", \"skra-sorsk\", \"skra-sorsk-egw\");\nawait moveFile(\"bestiary/npc\", \"skriss\", \"skriss-oota\");\nawait moveFile(\"bestiary/npc\", \"skylla\", \"skylla-wbtw\");\nawait moveFile(\"bestiary/npc\", \"sladis-vadir\", \"sladis-vadir-oota\");\nawait moveFile(\"bestiary/npc\", \"slarkrethel\", \"slarkrethel-skt\");\nawait moveFile(\"bestiary/npc\", \"sloopidoop\", \"sloopidoop-oota\");\nawait moveFile(\"bestiary/npc\", \"slurmy\", \"slurmy-nrh-tcmc\");\nawait moveFile(\"bestiary/npc\", \"smiler-the-defiler\", \"smiler-the-defiler-bgdia\");\nawait moveFile(\"bestiary/npc\", \"snarla\", \"snarla-tftyp\");\nawait moveFile(\"bestiary/npc\", \"snurrevin\", \"snurrevin-tftyp\");\nawait moveFile(\"bestiary/npc\", \"soluun-xibrindas\", \"soluun-xibrindas-wdh\");\nawait moveFile(\"bestiary/npc\", \"sovereign-basidia\", \"sovereign-basidia-oota\");\nawait moveFile(\"bestiary/npc\", \"spellix-romwod\", \"spellix-romwod-idrotf\");\nawait moveFile(\"bestiary/npc\", \"spider-king\", \"spider-king-oota\");\nawait moveFile(\"bestiary/npc\", \"spiderbait\", \"spiderbait-oota\");\nawait moveFile(\"bestiary/npc\", \"splugoth-the-returned\", \"splugoth-the-returned-ai\");\nawait moveFile(\"bestiary/npc\", \"squiddly\", \"squiddly-wdh\");\nawait moveFile(\"bestiary/npc\", \"squirt-the-oilcan\", \"squirt-the-oilcan-wbtw\");\nawait moveFile(\"bestiary/npc\", \"ssendam-lord-of-madness\", \"ssendam-lord-of-madness-mabjov\");\nawait moveFile(\"bestiary/npc\", \"stalagma-steelshadow\", \"stalagma-steelshadow-wdmm\");\nawait moveFile(\"bestiary/npc\", \"stanimir\", \"stanimir-cos\");\nawait moveFile(\"bestiary/npc\", \"steel-crane\", \"steel-crane-cm\");\nawait moveFile(\"bestiary/npc\", \"stella-wachter\", \"stella-wachter-cos\");\nawait moveFile(\"bestiary/npc\", \"stolos\", \"stolos-imr\");\nawait moveFile(\"bestiary/npc\", \"stool\", \"stool-oota\");\nawait moveFile(\"bestiary/npc\", \"strahd-von-zarovich\", \"strahd-von-zarovich-cos\");\nawait moveFile(\"bestiary/npc\", \"strahds-animated-armor\", \"strahds-animated-armor-cos\");\nawait moveFile(\"bestiary/npc\", \"strongheart\", \"strongheart-wbtw\");\nawait moveFile(\"bestiary/npc\", \"sul-khatesh\", \"sul-khatesh-erlw\");\nawait moveFile(\"bestiary/npc\", \"suldil-baldoriel\", \"suldil-baldoriel-mabjov\");\nawait moveFile(\"bestiary/npc\", \"sundeth\", \"sundeth-wdmm\");\nawait moveFile(\"bestiary/npc\", \"sylgar\", \"sylgar-wdh\");\nawait moveFile(\"bestiary/npc\", \"sylvira-savikas\", \"sylvira-savikas-bgdia\");\nawait moveFile(\"bestiary/npc\", \"syndra-silvane\", \"syndra-silvane-toa\");\nawait moveFile(\"bestiary/npc\", \"szikzith\", \"szikzith-nrh-awol\");\nawait moveFile(\"bestiary/npc\", \"szoldar-szoldarovich\", \"szoldar-szoldarovich-cos\");\nawait moveFile(\"bestiary/npc\", \"talis-the-white\", \"talis-the-white-hotdq\");\nawait moveFile(\"bestiary/npc\", \"talisolvanar-tally-fellbranch\", \"talisolvanar-tally-fellbranch-wdh\");\nawait moveFile(\"bestiary/npc\", \"tanazir-quandrix\", \"tanazir-quandrix-scc\");\nawait moveFile(\"bestiary/npc\", \"tarak\", \"tarak-dosi\");\nawait moveFile(\"bestiary/npc\", \"tarnhem\", \"tarnhem-imr\");\nawait moveFile(\"bestiary/npc\", \"tartha\", \"tartha-skt\");\nawait moveFile(\"bestiary/npc\", \"tarul-var\", \"tarul-var-tftyp\");\nawait moveFile(\"bestiary/npc\", \"tashlyn-yafeera\", \"tashlyn-yafeera-wdh\");\nawait moveFile(\"bestiary/npc\", \"tasloi\", \"tasloi-mabjov\");\nawait moveFile(\"bestiary/npc\", \"tasloi-sniper\", \"tasloi-sniper-mabjov\");\nawait moveFile(\"bestiary/npc\", \"tau\", \"tau-skt\");\nawait moveFile(\"bestiary/npc\", \"tecuziztecatl\", \"tecuziztecatl-tftyp\");\nawait moveFile(\"bestiary/npc\", \"terenzio-cassalanter\", \"terenzio-cassalanter-wdh\");\nawait moveFile(\"bestiary/npc\", \"thane-kayalithica\", \"thane-kayalithica-skt\");\nawait moveFile(\"bestiary/npc\", \"thavius-kreeg\", \"thavius-kreeg-bgdia\");\nawait moveFile(\"bestiary/npc\", \"the-abbot\", \"the-abbot-cos\");\nawait moveFile(\"bestiary/npc\", \"the-demogorgon\", \"the-demogorgon-imr\");\nawait moveFile(\"bestiary/npc\", \"the-keeper\", \"the-keeper-tftyp\");\nawait moveFile(\"bestiary/npc\", \"the-lord-of-blades\", \"the-lord-of-blades-erlw\");\nawait moveFile(\"bestiary/npc\", \"the-mad-mage-of-mount-baratok\", \"the-mad-mage-of-mount-baratok-cos\");\nawait moveFile(\"bestiary/npc\", \"the-pudding-king\", \"the-pudding-king-oota\");\nawait moveFile(\"bestiary/npc\", \"the-weevil\", \"the-weevil-skt\");\nawait moveFile(\"bestiary/npc\", \"themberchaud\", \"themberchaud-oota\");\nawait moveFile(\"bestiary/npc\", \"therzt\", \"therzt-tftyp\");\nawait moveFile(\"bestiary/npc\", \"thessalar\", \"thessalar-imr\");\nawait moveFile(\"bestiary/npc\", \"thessalars-homunculus\", \"thessalars-homunculus-imr\");\nawait moveFile(\"bestiary/npc\", \"thinnings\", \"thinnings-wbtw\");\nawait moveFile(\"bestiary/npc\", \"tholtz-daggerdark\", \"tholtz-daggerdark-skt\");\nawait moveFile(\"bestiary/npc\", \"thomas-t-toad\", \"thomas-t-toad-rmbre\");\nawait moveFile(\"bestiary/npc\", \"thornboldt-thorn-durst\", \"thornboldt-thorn-durst-cos\");\nawait moveFile(\"bestiary/npc\", \"thorvin-twinbeard\", \"thorvin-twinbeard-wdh\");\nawait moveFile(\"bestiary/npc\", \"thousand-teeth\", \"thousand-teeth-gos\");\nawait moveFile(\"bestiary/npc\", \"thrakkus\", \"thrakkus-wdh\");\nawait moveFile(\"bestiary/npc\", \"three-earrings\", \"three-earrings-egw\");\nawait moveFile(\"bestiary/npc\", \"thurl-merosska\", \"thurl-merosska-pota\");\nawait moveFile(\"bestiary/npc\", \"thurstwell-vanthampur\", \"thurstwell-vanthampur-bgdia\");\nawait moveFile(\"bestiary/npc\", \"thwad-underbrew\", \"thwad-underbrew-wdmm\");\nawait moveFile(\"bestiary/npc\", \"tiamat\", \"tiamat-rot\");\nawait moveFile(\"bestiary/npc\", \"tiberius-inuus\", \"tiberius-inuus-mabjov\");\nawait moveFile(\"bestiary/npc\", \"tissina-khyret\", \"tissina-khyret-wdh\");\nawait moveFile(\"bestiary/npc\", \"titivilus\", \"titivilus-mpmm\");\nawait moveFile(\"bestiary/npc\", \"tloques-popolocas\", \"tloques-popolocas-tftyp\");\nawait moveFile(\"bestiary/npc\", \"tommy-two-butts\", \"tommy-two-butts-rmbre\");\nawait moveFile(\"bestiary/npc\", \"tonalli\", \"tonalli-jttrc\");\nawait moveFile(\"bestiary/npc\", \"tooth-n-claw\", \"tooth-n-claw-slw\");\nawait moveFile(\"bestiary/npc\", \"topolah\", \"topolah-lox\");\nawait moveFile(\"bestiary/npc\", \"topsy\", \"topsy-oota\");\nawait moveFile(\"bestiary/npc\", \"torbit\", \"torbit-wdmm\");\nawait moveFile(\"bestiary/npc\", \"torlin-silvershield\", \"torlin-silvershield-tftyp\");\nawait moveFile(\"bestiary/npc\", \"tornscale\", \"tornscale-pota\");\nawait moveFile(\"bestiary/npc\", \"torogar-steelfist\", \"torogar-steelfist-bgdia\");\nawait moveFile(\"bestiary/npc\", \"traxigor\", \"traxigor-bgdia\");\nawait moveFile(\"bestiary/npc\", \"trenzia\", \"trenzia-wdmm\");\nawait moveFile(\"bestiary/npc\", \"trepsin\", \"trepsin-hotdq\");\nawait moveFile(\"bestiary/npc\", \"trobriand\", \"trobriand-wdmm\");\nawait moveFile(\"bestiary/npc\", \"tromokratis\", \"tromokratis-mot\");\nawait moveFile(\"bestiary/npc\", \"trostani\", \"trostani-ggr\");\nawait moveFile(\"bestiary/npc\", \"tug\", \"tug-skt\");\nawait moveFile(\"bestiary/npc\", \"tungsten-ward\", \"tungsten-ward-jttrc\");\nawait moveFile(\"bestiary/npc\", \"turlang\", \"turlang-skt\");\nawait moveFile(\"bestiary/npc\", \"turvy\", \"turvy-oota\");\nawait moveFile(\"bestiary/npc\", \"two-dry-cloaks\", \"two-dry-cloaks-oow\");\nawait moveFile(\"bestiary/npc\", \"tyreus-illusionist\", \"tyreus-illusionist-aitfr-fcd\");\nawait moveFile(\"bestiary/npc\", \"ulamog\", \"ulamog-psz\");\nawait moveFile(\"bestiary/npc\", \"ulder-ravengard\", \"ulder-ravengard-bgdia\");\nawait moveFile(\"bestiary/npc\", \"umbraxakar\", \"umbraxakar-wdmm\");\nawait moveFile(\"bestiary/npc\", \"urgala-meltimer\", \"urgala-meltimer-skt\");\nawait moveFile(\"bestiary/npc\", \"urstul-floxin\", \"urstul-floxin-wdh\");\nawait moveFile(\"bestiary/npc\", \"urwin-martikov\", \"urwin-martikov-cos\");\nawait moveFile(\"bestiary/npc\", \"uzoma-baten\", \"uzoma-baten-jttrc\");\nawait moveFile(\"bestiary/npc\", \"vaal\", \"vaal-skt\");\nawait moveFile(\"bestiary/npc\", \"vaasha\", \"vaasha-skt\");\nawait moveFile(\"bestiary/npc\", \"vajra-safahr\", \"vajra-safahr-wdh\");\nawait moveFile(\"bestiary/npc\", \"valetta\", \"valetta-wdh\");\nawait moveFile(\"bestiary/npc\", \"valin-sarnaster\", \"valin-sarnaster-cm\");\nawait moveFile(\"bestiary/npc\", \"valindra-shadowmantle\", \"valindra-shadowmantle-toa\");\nawait moveFile(\"bestiary/npc\", \"valtagar-steelshadow\", \"valtagar-steelshadow-wdmm\");\nawait moveFile(\"bestiary/npc\", \"valygar\", \"valygar-mabjov\");\nawait moveFile(\"bestiary/npc\", \"vanifer\", \"vanifer-pota\");\nawait moveFile(\"bestiary/npc\", \"varnoth\", \"varnoth-dosi\");\nawait moveFile(\"bestiary/npc\", \"varnyr\", \"varnyr-cm\");\nawait moveFile(\"bestiary/npc\", \"varram\", \"varram-rot\");\nawait moveFile(\"bestiary/npc\", \"vasilka\", \"vasilka-cos\");\nawait moveFile(\"bestiary/npc\", \"vecna-the-archlich\", \"vecna-the-archlich-vd\");\nawait moveFile(\"bestiary/npc\", \"veldyskar\", \"veldyskar-oota\");\nawait moveFile(\"bestiary/npc\", \"vellin-farstride\", \"vellin-farstride-mabjov\");\nawait moveFile(\"bestiary/npc\", \"vellynne-harpell\", \"vellynne-harpell-idrotf\");\nawait moveFile(\"bestiary/npc\", \"velomachus-lorehold\", \"velomachus-lorehold-scc\");\nawait moveFile(\"bestiary/npc\", \"venomfang\", \"venomfang-lmop\");\nawait moveFile(\"bestiary/npc\", \"verin-thelyss\", \"verin-thelyss-crcotn\");\nawait moveFile(\"bestiary/npc\", \"vertrand-shadowdusk\", \"vertrand-shadowdusk-wdmm\");\nawait moveFile(\"bestiary/npc\", \"vi-aroon\", \"vi-aroon-jttrc\");\nawait moveFile(\"bestiary/npc\", \"viari\", \"viari-ai\");\nawait moveFile(\"bestiary/npc\", \"viconia-devir\", \"viconia-devir-mabjov\");\nawait moveFile(\"bestiary/npc\", \"victor-vallakovich\", \"victor-vallakovich-cos\");\nawait moveFile(\"bestiary/npc\", \"victoro-cassalanter\", \"victoro-cassalanter-wdh\");\nawait moveFile(\"bestiary/npc\", \"viktor\", \"viktor-mabjov\");\nawait moveFile(\"bestiary/npc\", \"viln-tirin\", \"viln-tirin-oota\");\nawait moveFile(\"bestiary/npc\", \"vilnius\", \"vilnius-cos\");\nawait moveFile(\"bestiary/npc\", \"vincent-trench\", \"vincent-trench-wdh\");\nawait moveFile(\"bestiary/npc\", \"vizeran-devir\", \"vizeran-devir-oota\");\nawait moveFile(\"bestiary/npc\", \"vladimir-horngaard\", \"vladimir-horngaard-cos\");\nawait moveFile(\"bestiary/npc\", \"vocath\", \"vocath-lox\");\nawait moveFile(\"bestiary/npc\", \"volenta-popofsky\", \"volenta-popofsky-cos\");\nawait moveFile(\"bestiary/npc\", \"volothamp-volo-geddarm\", \"volothamp-volo-geddarm-toa\");\nawait moveFile(\"bestiary/npc\", \"wakanga-otamu\", \"wakanga-otamu-toa\");\nawait moveFile(\"bestiary/npc\", \"walnut-dankgrass\", \"walnut-dankgrass-ai\");\nawait moveFile(\"bestiary/npc\", \"warduke\", \"warduke-wbtw\");\nawait moveFile(\"bestiary/npc\", \"warwyck-blastimoff\", \"warwyck-blastimoff-lox\");\nawait moveFile(\"bestiary/npc\", \"wei-feng-ying\", \"wei-feng-ying-jttrc\");\nawait moveFile(\"bestiary/npc\", \"west-wind\", \"west-wind-llk\");\nawait moveFile(\"bestiary/npc\", \"white-jade-emperor\", \"white-jade-emperor-jttrc\");\nawait moveFile(\"bestiary/npc\", \"white-maw\", \"white-maw-tftyp\");\nawait moveFile(\"bestiary/npc\", \"whymsee\", \"whymsee-lr\");\nawait moveFile(\"bestiary/npc\", \"wiggan-nettlebee\", \"wiggan-nettlebee-pota\");\nawait moveFile(\"bestiary/npc\", \"willifort-crowelle\", \"willifort-crowelle-wdh\");\nawait moveFile(\"bestiary/npc\", \"windharrow\", \"windharrow-pota\");\nawait moveFile(\"bestiary/npc\", \"wine-weird\", \"wine-weird-toa\");\nawait moveFile(\"bestiary/npc\", \"wiri-fleagol\", \"wiri-fleagol-skt\");\nawait moveFile(\"bestiary/npc\", \"withers\", \"withers-toa\");\nawait moveFile(\"bestiary/npc\", \"wyllow\", \"wyllow-wdmm\");\nawait moveFile(\"bestiary/npc\", \"xan-moonblade\", \"xan-moonblade-mabjov\");\nawait moveFile(\"bestiary/npc\", \"xanathar\", \"xanathar-wdh\");\nawait moveFile(\"bestiary/npc\", \"xandala\", \"xandala-toa\");\nawait moveFile(\"bestiary/npc\", \"xardorok-sunblight\", \"xardorok-sunblight-idrotf\");\nawait moveFile(\"bestiary/npc\", \"xazax-the-eyemonger\", \"xazax-the-eyemonger-oota\");\nawait moveFile(\"bestiary/npc\", \"xilonen\", \"xilonen-tftyp\");\nawait moveFile(\"bestiary/npc\", \"xocopol\", \"xocopol-jttrc\");\nawait moveFile(\"bestiary/npc\", \"xolkin-alassandar\", \"xolkin-alassandar-skt\");\nawait moveFile(\"bestiary/npc\", \"xot\", \"xot-crcotn\");\nawait moveFile(\"bestiary/npc\", \"xzar-the-chaos-clone\", \"xzar-the-chaos-clone-mabjov\");\nawait moveFile(\"bestiary/npc\", \"y\", \"y-oota\");\nawait moveFile(\"bestiary/npc\", \"yagra-stonefist\", \"yagra-stonefist-wdh\");\nawait moveFile(\"bestiary/npc\", \"yalaga-maladwyn\", \"yalaga-maladwyn-gos\");\nawait moveFile(\"bestiary/npc\", \"yalah-gralhund\", \"yalah-gralhund-wdh\");\nawait moveFile(\"bestiary/npc\", \"yan-c-bin\", \"yan-c-bin-pota\");\nawait moveFile(\"bestiary/npc\", \"yarana\", \"yarana-jttrc\");\nawait moveFile(\"bestiary/npc\", \"yarnspinner\", \"yarnspinner-dod\");\nawait moveFile(\"bestiary/npc\", \"ydemi\", \"ydemi-scc\");\nawait moveFile(\"bestiary/npc\", \"yeenoghu\", \"yeenoghu-mpmm\");\nawait moveFile(\"bestiary/npc\", \"yestabrod\", \"yestabrod-oota\");\nawait moveFile(\"bestiary/npc\", \"yevgeni-krushkin\", \"yevgeni-krushkin-cos\");\nawait moveFile(\"bestiary/npc\", \"ygorl-lord-of-entropy\", \"ygorl-lord-of-entropy-mff\");\nawait moveFile(\"bestiary/npc\", \"yinra-emberwind\", \"yinra-emberwind-egw\");\nawait moveFile(\"bestiary/npc\", \"yorb\", \"yorb-toa\");\nawait moveFile(\"bestiary/npc\", \"yorn\", \"yorn-wdh\");\nawait moveFile(\"bestiary/npc\", \"young-gi\", \"young-gi-jttrc\");\nawait moveFile(\"bestiary/npc\", \"yuk-yuk\", \"yuk-yuk-oota\");\nawait moveFile(\"bestiary/npc\", \"yusdrayl\", \"yusdrayl-tftyp\");\nawait moveFile(\"bestiary/npc\", \"zalkore\", \"zalkore-toa\");\nawait moveFile(\"bestiary/npc\", \"zaltember\", \"zaltember-skt\");\nawait moveFile(\"bestiary/npc\", \"zalthar-shadowdusk\", \"zalthar-shadowdusk-wdmm\");\nawait moveFile(\"bestiary/npc\", \"zarak\", \"zarak-wbtw\");\nawait moveFile(\"bestiary/npc\", \"zargash\", \"zargash-wbtw\");\nawait moveFile(\"bestiary/npc\", \"zariel\", \"zariel-mpmm\");\nawait moveFile(\"bestiary/npc\", \"zaroum-al-saryak\", \"zaroum-al-saryak-toa\");\nawait moveFile(\"bestiary/npc\", \"zegana\", \"zegana-ggr\");\nawait moveFile(\"bestiary/npc\", \"zegdar\", \"zegdar-pota\");\nawait moveFile(\"bestiary/npc\", \"zephyros\", \"zephyros-skt\");\nawait moveFile(\"bestiary/npc\", \"zhanthi\", \"zhanthi-toa\");\nawait moveFile(\"bestiary/npc\", \"zi-liang\", \"zi-liang-skt\");\nawait moveFile(\"bestiary/npc\", \"zikran\", \"zikran-cm\");\nawait moveFile(\"bestiary/npc\", \"zikzokrishka\", \"zikzokrishka-cm\");\nawait moveFile(\"bestiary/npc\", \"zilchyn-qleptin\", \"zilchyn-qleptin-oota\");\nawait moveFile(\"bestiary/npc\", \"zindar\", \"zindar-toa\");\nawait moveFile(\"bestiary/npc\", \"ziraj-the-hunter\", \"ziraj-the-hunter-wdh\");\nawait moveFile(\"bestiary/npc\", \"zisatta\", \"zisatta-jttrc\");\nawait moveFile(\"bestiary/npc\", \"zorak-lightdrinker\", \"zorak-lightdrinker-wdmm\");\nawait moveFile(\"bestiary/npc\", \"zox-clammersham\", \"zox-clammersham-wdmm\");\nawait moveFile(\"bestiary/npc\", \"zress-orlezziir\", \"zress-orlezziir-wdmm\");\nawait moveFile(\"bestiary/npc\", \"zuggtmoy\", \"zuggtmoy-mpmm\");\nawait moveFile(\"bestiary/npc\", \"zuleika-toranescu\", \"zuleika-toranescu-cos\");\nawait moveFile(\"bestiary/npc\", \"zygfrek-belview\", \"zygfrek-belview-cos\");\nawait moveFile(\"bestiary/ooze\", \"adult-oblex\", \"adult-oblex-mpmm\");\nawait moveFile(\"bestiary/ooze\", \"dragonblood-ooze\", \"dragonblood-ooze-ftd\");\nawait moveFile(\"bestiary/ooze\", \"elder-black-pudding\", \"elder-black-pudding-tftyp\");\nawait moveFile(\"bestiary/ooze\", \"elder-oblex\", \"elder-oblex-mpmm\");\nawait moveFile(\"bestiary/ooze\", \"huge-gray-ooze\", \"huge-gray-ooze-wdmm\");\nawait moveFile(\"bestiary/ooze\", \"huge-ochre-jelly\", \"huge-ochre-jelly-tftyp\");\nawait moveFile(\"bestiary/ooze\", \"inkling-mascot\", \"inkling-mascot-scc\");\nawait moveFile(\"bestiary/ooze\", \"intelligent-black-pudding\", \"intelligent-black-pudding-wdmm\");\nawait moveFile(\"bestiary/ooze\", \"oblex-spawn\", \"oblex-spawn-mpmm\");\nawait moveFile(\"bestiary/ooze\", \"ooze-folk\", \"ooze-folk-llk\");\nawait moveFile(\"bestiary/ooze\", \"plasmoid-boss\", \"plasmoid-boss-bam\");\nawait moveFile(\"bestiary/ooze\", \"plasmoid-explorer\", \"plasmoid-explorer-bam\");\nawait moveFile(\"bestiary/ooze\", \"plasmoid-warrior\", \"plasmoid-warrior-bam\");\nawait moveFile(\"bestiary/ooze\", \"reduced-threat-black-pudding\", \"reduced-threat-black-pudding-tftyp\");\nawait moveFile(\"bestiary/ooze\", \"reduced-threat-gray-ooze\", \"reduced-threat-gray-ooze-tftyp\");\nawait moveFile(\"bestiary/ooze\", \"reduced-threat-ochre-jelly\", \"reduced-threat-ochre-jelly-tftyp\");\nawait moveFile(\"bestiary/ooze\", \"regenerating-black-pudding\", \"regenerating-black-pudding-oota\");\nawait moveFile(\"bestiary/ooze\", \"sentient-gray-ooze\", \"sentient-gray-ooze-tftyp\");\nawait moveFile(\"bestiary/ooze\", \"sentient-ochre-jelly\", \"sentient-ochre-jelly-tftyp\");\nawait moveFile(\"bestiary/ooze\", \"slithering-tracker\", \"slithering-tracker-mpmm\");\nawait moveFile(\"bestiary/plant\", \"aartuk-elder\", \"aartuk-elder-bam\");\nawait moveFile(\"bestiary/plant\", \"aartuk-priest\", \"aartuk-priest-bam\");\nawait moveFile(\"bestiary/plant\", \"aartuk-warrior\", \"aartuk-warrior-bam\");\nawait moveFile(\"bestiary/plant\", \"animated-tree\", \"animated-tree-egw\");\nawait moveFile(\"bestiary/plant\", \"assassin-vine\", \"assassin-vine-toa\");\nawait moveFile(\"bestiary/plant\", \"astral-blight\", \"astral-blight-lox\");\nawait moveFile(\"bestiary/plant\", \"awakened-zurkhwood\", \"awakened-zurkhwood-oota\");\nawait moveFile(\"bestiary/plant\", \"bodytaker-plant\", \"bodytaker-plant-vrgr\");\nawait moveFile(\"bestiary/plant\", \"brackish-trudge\", \"brackish-trudge-scc\");\nawait moveFile(\"bestiary/plant\", \"bridesmaid-of-zuggtmoy\", \"bridesmaid-of-zuggtmoy-oota\");\nawait moveFile(\"bestiary/plant\", \"campestri\", \"campestri-wbtw\");\nawait moveFile(\"bestiary/plant\", \"carnivorous-flower\", \"carnivorous-flower-rot\");\nawait moveFile(\"bestiary/plant\", \"chamberlain-of-zuggtmoy\", \"chamberlain-of-zuggtmoy-oota\");\nawait moveFile(\"bestiary/plant\", \"chuul-spore-servant\", \"chuul-spore-servant-oota\");\nawait moveFile(\"bestiary/plant\", \"corpse-flower\", \"corpse-flower-mpmm\");\nawait moveFile(\"bestiary/plant\", \"drow-spore-servant\", \"drow-spore-servant-oota\");\nawait moveFile(\"bestiary/plant\", \"duergar-spore-servant\", \"duergar-spore-servant-oota\");\nawait moveFile(\"bestiary/plant\", \"gadabout\", \"gadabout-mcv1sc\");\nawait moveFile(\"bestiary/plant\", \"groff\", \"groff-scc\");\nawait moveFile(\"bestiary/plant\", \"hook-horror-spore-servant\", \"hook-horror-spore-servant-oota\");\nawait moveFile(\"bestiary/plant\", \"jammer-leech\", \"jammer-leech-bam\");\nawait moveFile(\"bestiary/plant\", \"kelpie\", \"kelpie-tftyp\");\nawait moveFile(\"bestiary/plant\", \"lycanthropickle\", \"lycanthropickle-rmbre\");\nawait moveFile(\"bestiary/plant\", \"mantrap\", \"mantrap-toa\");\nawait moveFile(\"bestiary/plant\", \"needle-lord\", \"needle-lord-mff\");\nawait moveFile(\"bestiary/plant\", \"needle-spawn\", \"needle-spawn-mff\");\nawait moveFile(\"bestiary/plant\", \"podling\", \"podling-vrgr\");\nawait moveFile(\"bestiary/plant\", \"spore-servant-octopus\", \"spore-servant-octopus-dosi\");\nawait moveFile(\"bestiary/plant\", \"swarm-of-campestris\", \"swarm-of-campestris-wbtw\");\nawait moveFile(\"bestiary/plant\", \"thorn-slinger\", \"thorn-slinger-tftyp\");\nawait moveFile(\"bestiary/plant\", \"thorny-vegepygmy\", \"thorny-vegepygmy-mpmm\");\nawait moveFile(\"bestiary/plant\", \"treant-sapling\", \"treant-sapling-wbtw\");\nawait moveFile(\"bestiary/plant\", \"tree-blight\", \"tree-blight-cos\");\nawait moveFile(\"bestiary/plant\", \"tri-flower-frond\", \"tri-flower-frond-toa\");\nawait moveFile(\"bestiary/plant\", \"tribal-warrior-spore-servants\", \"tribal-warrior-spore-servants-idrotf\");\nawait moveFile(\"bestiary/plant\", \"vegepygmy-chief\", \"vegepygmy-chief-mpmm\");\nawait moveFile(\"bestiary/plant\", \"vegepygmy\", \"vegepygmy-mpmm\");\nawait moveFile(\"bestiary/plant\", \"wood-woad\", \"wood-woad-mpmm\");\nawait moveFile(\"bestiary/plant\", \"yellow-musk-creeper\", \"yellow-musk-creeper-toa\");\nawait moveFile(\"bestiary/plant\", \"yggdrasti\", \"yggdrasti-mcv1sc\");\nawait moveFile(\"bestiary/undead\", \"adult-red-dracolich\", \"adult-red-dracolich-tce\");\nawait moveFile(\"bestiary/undead\", \"alhoon\", \"alhoon-mpmm\");\nawait moveFile(\"bestiary/undead\", \"allip\", \"allip-mpmm\");\nawait moveFile(\"bestiary/undead\", \"amonkhet-mummy-lord\", \"amonkhet-mummy-lord-psa\");\nawait moveFile(\"bestiary/undead\", \"amonkhet-mummy\", \"amonkhet-mummy-psa\");\nawait moveFile(\"bestiary/undead\", \"ankylosaurus-zombie\", \"ankylosaurus-zombie-toa\");\nawait moveFile(\"bestiary/undead\", \"anointed\", \"anointed-psa\");\nawait moveFile(\"bestiary/undead\", \"aquatic-ghoul\", \"aquatic-ghoul-pota\");\nawait moveFile(\"bestiary/undead\", \"ash-zombie\", \"ash-zombie-lmop\");\nawait moveFile(\"bestiary/undead\", \"atropal\", \"atropal-toa\");\nawait moveFile(\"bestiary/undead\", \"blood-drinker-vampire\", \"blood-drinker-vampire-ggr\");\nawait moveFile(\"bestiary/undead\", \"bodak\", \"bodak-mpmm\");\nawait moveFile(\"bestiary/undead\", \"boneclaw\", \"boneclaw-mpmm\");\nawait moveFile(\"bestiary/undead\", \"boneless\", \"boneless-vrgr\");\nawait moveFile(\"bestiary/undead\", \"brain-in-a-jar\", \"brain-in-a-jar-llk\");\nawait moveFile(\"bestiary/undead\", \"centaur-mummy\", \"centaur-mummy-tftyp\");\nawait moveFile(\"bestiary/undead\", \"cloud-giant-ghost\", \"cloud-giant-ghost-cm\");\nawait moveFile(\"bestiary/undead\", \"coldlight-walker\", \"coldlight-walker-idrotf\");\nawait moveFile(\"bestiary/undead\", \"deathlock-mastermind\", \"deathlock-mastermind-mpmm\");\nawait moveFile(\"bestiary/undead\", \"deathlock\", \"deathlock-mpmm\");\nawait moveFile(\"bestiary/undead\", \"deathlock-wight\", \"deathlock-wight-mpmm\");\nawait moveFile(\"bestiary/undead\", \"deaths-head\", \"deaths-head-vrgr\");\nawait moveFile(\"bestiary/undead\", \"devkarin-lich\", \"devkarin-lich-ggr\");\nawait moveFile(\"bestiary/undead\", \"devourer\", \"devourer-mpmm\");\nawait moveFile(\"bestiary/undead\", \"dinosaur-skeleton\", \"dinosaur-skeleton-jttrc\");\nawait moveFile(\"bestiary/undead\", \"draconic-shard\", \"draconic-shard-ftd\");\nawait moveFile(\"bestiary/undead\", \"dread-warrior\", \"dread-warrior-tftyp\");\nawait moveFile(\"bestiary/undead\", \"drowned-ascetic\", \"drowned-ascetic-gos\");\nawait moveFile(\"bestiary/undead\", \"drowned-assassin\", \"drowned-assassin-gos\");\nawait moveFile(\"bestiary/undead\", \"drowned-blade\", \"drowned-blade-gos\");\nawait moveFile(\"bestiary/undead\", \"drowned-master\", \"drowned-master-gos\");\nawait moveFile(\"bestiary/undead\", \"dryad-spirit\", \"dryad-spirit-bgdia\");\nawait moveFile(\"bestiary/undead\", \"dullahan\", \"dullahan-vrgr\");\nawait moveFile(\"bestiary/undead\", \"eidolon\", \"eidolon-mpmm\");\nawait moveFile(\"bestiary/undead\", \"eldritch-lich\", \"eldritch-lich-mcv1sc\");\nawait moveFile(\"bestiary/undead\", \"eternal\", \"eternal-psa\");\nawait moveFile(\"bestiary/undead\", \"eye-of-fear-and-flame\", \"eye-of-fear-and-flame-mff\");\nawait moveFile(\"bestiary/undead\", \"flitterstep-eidolon\", \"flitterstep-eidolon-mot\");\nawait moveFile(\"bestiary/undead\", \"frost-giant-skeleton\", \"frost-giant-skeleton-idrotf\");\nawait moveFile(\"bestiary/undead\", \"frost-giant-zombie\", \"frost-giant-zombie-egw\");\nawait moveFile(\"bestiary/undead\", \"fungal-servant\", \"fungal-servant-cm\");\nawait moveFile(\"bestiary/undead\", \"gallows-speaker\", \"gallows-speaker-vrgr\");\nawait moveFile(\"bestiary/undead\", \"geist\", \"geist-psi\");\nawait moveFile(\"bestiary/undead\", \"ghost-dragon\", \"ghost-dragon-ftd\");\nawait moveFile(\"bestiary/undead\", \"ghostblade-eidolon\", \"ghostblade-eidolon-mot\");\nawait moveFile(\"bestiary/undead\", \"giant-shark-skeleton\", \"giant-shark-skeleton-sdw\");\nawait moveFile(\"bestiary/undead\", \"giant-skeleton\", \"giant-skeleton-tftyp\");\nawait moveFile(\"bestiary/undead\", \"giant-zombie-constrictor-snake\", \"giant-zombie-constrictor-snake-aitfr-dn\");\nawait moveFile(\"bestiary/undead\", \"girallon-zombie\", \"girallon-zombie-toa\");\nawait moveFile(\"bestiary/undead\", \"gloamwing\", \"gloamwing-ggr\");\nawait moveFile(\"bestiary/undead\", \"gnoll-vampire\", \"gnoll-vampire-idrotf\");\nawait moveFile(\"bestiary/undead\", \"gnoll-witherling\", \"gnoll-witherling-mpmm\");\nawait moveFile(\"bestiary/undead\", \"greater-zombie\", \"greater-zombie-tftyp\");\nawait moveFile(\"bestiary/undead\", \"haint\", \"haint-jttrc\");\nawait moveFile(\"bestiary/undead\", \"hollow-dragon\", \"hollow-dragon-ftd\");\nawait moveFile(\"bestiary/undead\", \"husk-zombie\", \"husk-zombie-egw\");\nawait moveFile(\"bestiary/undead\", \"icewind-kobold-zombie\", \"icewind-kobold-zombie-idrotf\");\nawait moveFile(\"bestiary/undead\", \"illithilich\", \"illithilich-vgm\");\nawait moveFile(\"bestiary/undead\", \"indentured-spirit\", \"indentured-spirit-ggr\");\nawait moveFile(\"bestiary/undead\", \"jiangshi\", \"jiangshi-vrgr\");\nawait moveFile(\"bestiary/undead\", \"karrnathi-undead-soldier\", \"karrnathi-undead-soldier-erlw\");\nawait moveFile(\"bestiary/undead\", \"knight-of-the-order\", \"knight-of-the-order-cos\");\nawait moveFile(\"bestiary/undead\", \"kobold-vampire-spawn\", \"kobold-vampire-spawn-idrotf\");\nawait moveFile(\"bestiary/undead\", \"lacedon\", \"lacedon-tftyp\");\nawait moveFile(\"bestiary/undead\", \"lesser-mummy-lord\", \"lesser-mummy-lord-tftyp\");\nawait moveFile(\"bestiary/undead\", \"lichen-lich\", \"lichen-lich-cm\");\nawait moveFile(\"bestiary/undead\", \"lonelywood-banshee\", \"lonelywood-banshee-idrotf\");\nawait moveFile(\"bestiary/undead\", \"mind-drinker-vampire\", \"mind-drinker-vampire-ggr\");\nawait moveFile(\"bestiary/undead\", \"necrichor\", \"necrichor-vrgr\");\nawait moveFile(\"bestiary/undead\", \"nightveil-specter\", \"nightveil-specter-ggr\");\nawait moveFile(\"bestiary/undead\", \"nightwalker\", \"nightwalker-mpmm\");\nawait moveFile(\"bestiary/undead\", \"nosferatu\", \"nosferatu-vrgr\");\nawait moveFile(\"bestiary/undead\", \"obzedat-ghost\", \"obzedat-ghost-ggr\");\nawait moveFile(\"bestiary/undead\", \"ogre-skeleton\", \"ogre-skeleton-tftyp\");\nawait moveFile(\"bestiary/undead\", \"ooze-master\", \"ooze-master-tftyp\");\nawait moveFile(\"bestiary/undead\", \"parasite-infested-behir\", \"parasite-infested-behir-cm\");\nawait moveFile(\"bestiary/undead\", \"phantom-warrior\", \"phantom-warrior-cos\");\nawait moveFile(\"bestiary/undead\", \"phylaskia\", \"phylaskia-mot\");\nawait moveFile(\"bestiary/undead\", \"reaper-spirit\", \"reaper-spirit-ua2022wondersofthemultiverse\");\nawait moveFile(\"bestiary/undead\", \"reduced-threat-wight\", \"reduced-threat-wight-tftyp\");\nawait moveFile(\"bestiary/undead\", \"returned-drifter\", \"returned-drifter-mot\");\nawait moveFile(\"bestiary/undead\", \"returned-kakomantis\", \"returned-kakomantis-mot\");\nawait moveFile(\"bestiary/undead\", \"returned-palamnite\", \"returned-palamnite-mot\");\nawait moveFile(\"bestiary/undead\", \"returned-sentry\", \"returned-sentry-mot\");\nawait moveFile(\"bestiary/undead\", \"shade\", \"shade-psz\");\nawait moveFile(\"bestiary/undead\", \"shadow-assassin\", \"shadow-assassin-wdmm\");\nawait moveFile(\"bestiary/undead\", \"shadowghast\", \"shadowghast-egw\");\nawait moveFile(\"bestiary/undead\", \"skeletal-alchemist\", \"skeletal-alchemist-gos\");\nawait moveFile(\"bestiary/undead\", \"skeletal-bloodfin\", \"skeletal-bloodfin-crcotn\");\nawait moveFile(\"bestiary/undead\", \"skeletal-giant-owl\", \"skeletal-giant-owl-imr\");\nawait moveFile(\"bestiary/undead\", \"skeletal-horror\", \"skeletal-horror-aitfr-dn\");\nawait moveFile(\"bestiary/undead\", \"skeletal-juggernaut\", \"skeletal-juggernaut-gos\");\nawait moveFile(\"bestiary/undead\", \"skeletal-owlbear\", \"skeletal-owlbear-imr\");\nawait moveFile(\"bestiary/undead\", \"skeletal-polar-bear\", \"skeletal-polar-bear-imr\");\nawait moveFile(\"bestiary/undead\", \"skeletal-rats\", \"skeletal-rats-bgdia\");\nawait moveFile(\"bestiary/undead\", \"skeletal-swarm\", \"skeletal-swarm-gos\");\nawait moveFile(\"bestiary/undead\", \"skeletal-two-headed-owlbear\", \"skeletal-two-headed-owlbear-imr\");\nawait moveFile(\"bestiary/undead\", \"skeleton-key\", \"skeleton-key-toa\");\nawait moveFile(\"bestiary/undead\", \"skeleton-lord\", \"skeleton-lord-mabjov\");\nawait moveFile(\"bestiary/undead\", \"skeleton-warrior\", \"skeleton-warrior-mabjov\");\nawait moveFile(\"bestiary/undead\", \"skull-lord\", \"skull-lord-mpmm\");\nawait moveFile(\"bestiary/undead\", \"snow-maiden\", \"snow-maiden-cos\");\nawait moveFile(\"bestiary/undead\", \"soul-shaker\", \"soul-shaker-jttrc\");\nawait moveFile(\"bestiary/undead\", \"spawn-of-kyuss\", \"spawn-of-kyuss-mpmm\");\nawait moveFile(\"bestiary/undead\", \"spirit\", \"spirit-psz\");\nawait moveFile(\"bestiary/undead\", \"stomping-foot\", \"stomping-foot-oow\");\nawait moveFile(\"bestiary/undead\", \"storm-giant-skeleton\", \"storm-giant-skeleton-cm\");\nawait moveFile(\"bestiary/undead\", \"strahd-zombie\", \"strahd-zombie-cos\");\nawait moveFile(\"bestiary/undead\", \"swarm-of-undead-snakes\", \"swarm-of-undead-snakes-egw\");\nawait moveFile(\"bestiary/undead\", \"swarm-of-zombie-limbs\", \"swarm-of-zombie-limbs-vrgr\");\nawait moveFile(\"bestiary/undead\", \"sword-wraith-commander\", \"sword-wraith-commander-mpmm\");\nawait moveFile(\"bestiary/undead\", \"sword-wraith-warrior\", \"sword-wraith-warrior-mpmm\");\nawait moveFile(\"bestiary/undead\", \"thunderbeast-skeleton\", \"thunderbeast-skeleton-skt\");\nawait moveFile(\"bestiary/undead\", \"tomb-dwarf\", \"tomb-dwarf-toa\");\nawait moveFile(\"bestiary/undead\", \"topi\", \"topi-ttp\");\nawait moveFile(\"bestiary/undead\", \"tyrannosaurus-zombie\", \"tyrannosaurus-zombie-toa\");\nawait moveFile(\"bestiary/undead\", \"undead-bulette\", \"undead-bulette-wdmm\");\nawait moveFile(\"bestiary/undead\", \"undead-cockatrice\", \"undead-cockatrice-oow\");\nawait moveFile(\"bestiary/undead\", \"undead-shambling-mound\", \"undead-shambling-mound-wdmm\");\nawait moveFile(\"bestiary/undead\", \"undead-spirit-3rd-level-spell\", \"undead-spirit-3rd-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/undead\", \"undead-spirit-4th-level-spell\", \"undead-spirit-4th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/undead\", \"undead-spirit-5th-level-spell\", \"undead-spirit-5th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/undead\", \"undead-spirit-6th-level-spell\", \"undead-spirit-6th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/undead\", \"undead-spirit-7th-level-spell\", \"undead-spirit-7th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/undead\", \"undead-spirit-8th-level-spell\", \"undead-spirit-8th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/undead\", \"undead-spirit-9th-level-spell\", \"undead-spirit-9th-level-spell-ua2020spellsandmagictattoos\");\nawait moveFile(\"bestiary/undead\", \"undead-spirit-ghostly-3rd-level-spell\", \"undead-spirit-ghostly-3rd-level-spell-tce\");\nawait moveFile(\"bestiary/undead\", \"undead-spirit-ghostly-4th-level-spell\", \"undead-spirit-ghostly-4th-level-spell-tce\");\nawait moveFile(\"bestiary/undead\", \"undead-spirit-ghostly-5th-level-spell\", \"undead-spirit-ghostly-5th-level-spell-tce\");\nawait moveFile(\"bestiary/undead\", \"undead-spirit-ghostly-6th-level-spell\", \"undead-spirit-ghostly-6th-level-spell-tce\");\nawait moveFile(\"bestiary/undead\", \"undead-spirit-ghostly-7th-level-spell\", \"undead-spirit-ghostly-7th-level-spell-tce\");\nawait moveFile(\"bestiary/undead\", \"undead-spirit-ghostly-8th-level-spell\", \"undead-spirit-ghostly-8th-level-spell-tce\");\nawait moveFile(\"bestiary/undead\", \"undead-spirit-ghostly-9th-level-spell\", \"undead-spirit-ghostly-9th-level-spell-tce\");\nawait moveFile(\"bestiary/undead\", \"undead-spirit-putrid-3rd-level-spell\", \"undead-spirit-putrid-3rd-level-spell-tce\");\nawait moveFile(\"bestiary/undead\", \"undead-spirit-putrid-4th-level-spell\", \"undead-spirit-putrid-4th-level-spell-tce\");\nawait moveFile(\"bestiary/undead\", \"undead-spirit-putrid-5th-level-spell\", \"undead-spirit-putrid-5th-level-spell-tce\");\nawait moveFile(\"bestiary/undead\", \"undead-spirit-putrid-6th-level-spell\", \"undead-spirit-putrid-6th-level-spell-tce\");\nawait moveFile(\"bestiary/undead\", \"undead-spirit-putrid-7th-level-spell\", \"undead-spirit-putrid-7th-level-spell-tce\");\nawait moveFile(\"bestiary/undead\", \"undead-spirit-putrid-8th-level-spell\", \"undead-spirit-putrid-8th-level-spell-tce\");\nawait moveFile(\"bestiary/undead\", \"undead-spirit-putrid-9th-level-spell\", \"undead-spirit-putrid-9th-level-spell-tce\");\nawait moveFile(\"bestiary/undead\", \"undead-spirit-skeletal-3rd-level-spell\", \"undead-spirit-skeletal-3rd-level-spell-tce\");\nawait moveFile(\"bestiary/undead\", \"undead-spirit-skeletal-4th-level-spell\", \"undead-spirit-skeletal-4th-level-spell-tce\");\nawait moveFile(\"bestiary/undead\", \"undead-spirit-skeletal-5th-level-spell\", \"undead-spirit-skeletal-5th-level-spell-tce\");\nawait moveFile(\"bestiary/undead\", \"undead-spirit-skeletal-6th-level-spell\", \"undead-spirit-skeletal-6th-level-spell-tce\");\nawait moveFile(\"bestiary/undead\", \"undead-spirit-skeletal-7th-level-spell\", \"undead-spirit-skeletal-7th-level-spell-tce\");\nawait moveFile(\"bestiary/undead\", \"undead-spirit-skeletal-8th-level-spell\", \"undead-spirit-skeletal-8th-level-spell-tce\");\nawait moveFile(\"bestiary/undead\", \"undead-spirit-skeletal-9th-level-spell\", \"undead-spirit-skeletal-9th-level-spell-tce\");\nawait moveFile(\"bestiary/undead\", \"undead-tree\", \"undead-tree-bgdia\");\nawait moveFile(\"bestiary/undead\", \"undying-councilor\", \"undying-councilor-erlw\");\nawait moveFile(\"bestiary/undead\", \"undying-soldier\", \"undying-soldier-erlw\");\nawait moveFile(\"bestiary/undead\", \"vampirate\", \"vampirate-bam\");\nawait moveFile(\"bestiary/undead\", \"vampirate-captain\", \"vampirate-captain-bam\");\nawait moveFile(\"bestiary/undead\", \"vampirate-mage\", \"vampirate-mage-bam\");\nawait moveFile(\"bestiary/undead\", \"vampire-neonate\", \"vampire-neonate-psi\");\nawait moveFile(\"bestiary/undead\", \"vampire-null\", \"vampire-null-psz\");\nawait moveFile(\"bestiary/undead\", \"vampiric-mind-flayer\", \"vampiric-mind-flayer-vrgr\");\nawait moveFile(\"bestiary/undead\", \"vampiric-mist\", \"vampiric-mist-mpmm\");\nawait moveFile(\"bestiary/undead\", \"warrior-spirit\", \"warrior-spirit-ua2022wondersofthemultiverse\");\nawait moveFile(\"bestiary/undead\", \"will-o-wells\", \"will-o-wells-wbtw\");\nawait moveFile(\"bestiary/undead\", \"yellow-musk-zombie\", \"yellow-musk-zombie-toa\");\nawait moveFile(\"bestiary/undead\", \"zombie-cat\", \"zombie-cat-psi\");\nawait moveFile(\"bestiary/undead\", \"zombie-clot\", \"zombie-clot-vrgr\");\nawait moveFile(\"bestiary/undead\", \"zombie-horse\", \"zombie-horse-psd\");\nawait moveFile(\"bestiary/undead\", \"zombie-plague-spreader\", \"zombie-plague-spreader-vrgr\");\nawait moveFile(\"bestiary/undead\", \"zombie-rat\", \"zombie-rat-psi\");\nawait moveFile(\"bestiary/undead\", \"zombie-snake\", \"zombie-snake-psi\");\nawait moveFile(\"classes\", \"artificer\", \"artificer-tce\");\nawait moveFile(\"classes\", \"alchemist\", \"artificer-alchemist-tce\");\nawait moveFile(\"classes\", \"artificer-alchemist\", \"artificer-alchemist-tce\");\nawait moveFile(\"classes\", \"armorer\", \"artificer-armorer-tce\");\nawait moveFile(\"classes\", \"artificer-armorer\", \"artificer-armorer-tce\");\nawait moveFile(\"classes\", \"artillerist\", \"artificer-artillerist-tce\");\nawait moveFile(\"classes\", \"artificer-artillerist\", \"artificer-artillerist-tce\");\nawait moveFile(\"classes\", \"battle-smith\", \"artificer-battle-smith-tce\");\nawait moveFile(\"classes\", \"artificer-battle-smith\", \"artificer-battle-smith-tce\");\nawait moveFile(\"classes\", \"gunsmith\", \"artificer-gunsmith-uaartificer\");\nawait moveFile(\"classes\", \"artificer-gunsmith\", \"artificer-gunsmith-uaartificer\");\nawait moveFile(\"classes\", \"artificer\", \"artificer-tce\");\nawait moveFile(\"classes\", \"path-of-the-ancestral-guardian\", \"barbarian-path-of-the-ancestral-guardian\");\nawait moveFile(\"classes\", \"path-of-the-battlerager\", \"barbarian-path-of-the-battlerager\");\nawait moveFile(\"classes\", \"path-of-the-beast\", \"barbarian-path-of-the-beast\");\nawait moveFile(\"classes\", \"path-of-the-berserker\", \"barbarian-path-of-the-berserker\");\nawait moveFile(\"classes\", \"path-of-the-giant-ua\", \"barbarian-path-of-the-giant-ua\");\nawait moveFile(\"classes\", \"path-of-the-storm-herald\", \"barbarian-path-of-the-storm-herald\");\nawait moveFile(\"classes\", \"path-of-the-totem-warrior\", \"barbarian-path-of-the-totem-warrior\");\nawait moveFile(\"classes\", \"path-of-the-zealot\", \"barbarian-path-of-the-zealot\");\nawait moveFile(\"classes\", \"path-of-wild-magic\", \"barbarian-path-of-wild-magic\");\n\nawait moveFile(\"classes\", \"college-of-creation\", \"bard-college-of-creation\");\nawait moveFile(\"classes\", \"college-of-eloquence\", \"bard-college-of-eloquence\");\nawait moveFile(\"classes\", \"college-of-glamour\", \"bard-college-of-glamour\");\nawait moveFile(\"classes\", \"college-of-lore\", \"bard-college-of-lore\");\nawait moveFile(\"classes\", \"college-of-satire-ua\", \"bard-college-of-satire-ua\");\nawait moveFile(\"classes\", \"college-of-spirits\", \"bard-college-of-spirits\");\nawait moveFile(\"classes\", \"college-of-swords\", \"bard-college-of-swords\");\nawait moveFile(\"classes\", \"college-of-valor\", \"bard-college-of-valor\");\nawait moveFile(\"classes\", \"college-of-whispers\", \"bard-college-of-whispers\");\n\nawait moveFile(\"classes\", \"mage-of-lorehold-ua\", \"bard-mage-of-lorehold-ua\");\nawait moveFile(\"classes\", \"mage-of-silverquill-ua\", \"bard-mage-of-silverquill-ua\");\n\nawait moveFile(\"classes\", \"ambition-domain-psa\", \"cleric-ambition-domain-psa\");\nawait moveFile(\"classes\", \"arcana-domain\", \"cleric-arcana-domain\");\nawait moveFile(\"classes\", \"city-domain-ua\", \"cleric-city-domain-ua\");\nawait moveFile(\"classes\", \"death-domain\", \"cleric-death-domain\");\nawait moveFile(\"classes\", \"fate-domain-ua\", \"cleric-fate-domain-ua\");\nawait moveFile(\"classes\", \"forge-domain\", \"cleric-forge-domain\");\nawait moveFile(\"classes\", \"grave-domain\", \"cleric-grave-domain\");\nawait moveFile(\"classes\", \"knowledge-domain\", \"cleric-knowledge-domain\");\nawait moveFile(\"classes\", \"life-domain\", \"cleric-life-domain\");\nawait moveFile(\"classes\", \"light-domain\", \"cleric-light-domain\");\nawait moveFile(\"classes\", \"nature-domain\", \"cleric-nature-domain\");\nawait moveFile(\"classes\", \"order-domain\", \"cleric-order-domain\");\nawait moveFile(\"classes\", \"peace-domain\", \"cleric-peace-domain\");\n\nawait moveFile(\"classes\", \"protection-domain-ua\", \"cleric-protection-domain-ua\");\nawait moveFile(\"classes\", \"solidarity-domain-psa\", \"cleric-solidarity-domain-psa\");\nawait moveFile(\"classes\", \"strength-domain-psa\", \"cleric-strength-domain-psa\");\nawait moveFile(\"classes\", \"tempest-domain\", \"cleric-tempest-domain\");\nawait moveFile(\"classes\", \"trickery-domain\", \"cleric-trickery-domain\");\nawait moveFile(\"classes\", \"twilight-domain\", \"cleric-twilight-domain\");\nawait moveFile(\"classes\", \"war-domain\", \"cleric-war-domain\");\nawait moveFile(\"classes\", \"zeal-domain-psa\", \"cleric-zeal-domain-psa\");\nawait moveFile(\"classes\", \"circle-of-dreams\", \"druid-circle-of-dreams\");\nawait moveFile(\"classes\", \"circle-of-spores\", \"druid-circle-of-spores\");\nawait moveFile(\"classes\", \"circle-of-stars\", \"druid-circle-of-stars\");\nawait moveFile(\"classes\", \"circle-of-the-land\", \"druid-circle-of-the-land\");\nawait moveFile(\"classes\", \"circle-of-the-moon\", \"druid-circle-of-the-moon\");\nawait moveFile(\"classes\", \"circle-of-the-primeval-ua\", \"druid-circle-of-the-primeval-ua\");\nawait moveFile(\"classes\", \"circle-of-the-shepherd\", \"druid-circle-of-the-shepherd\");\nawait moveFile(\"classes\", \"circle-of-twilight-ua\", \"druid-circle-of-twilight-ua\");\nawait moveFile(\"classes\", \"circle-of-wildfire\", \"druid-circle-of-wildfire\");\nawait moveFile(\"classes\", \"expert-sidekick\", \"expert-sidekick-tce\");\nawait moveFile(\"classes\", \"expert-sidekick-ua\", \"expert-sidekick-uasidekicks\");\nawait moveFile(\"classes\", \"arcane-archer\", \"fighter-arcane-archer\");\nawait moveFile(\"classes\", \"battle-master\", \"fighter-battle-master\");\nawait moveFile(\"classes\", \"brute-ua\", \"fighter-brute-ua\");\nawait moveFile(\"classes\", \"cavalier\", \"fighter-cavalier\");\nawait moveFile(\"classes\", \"champion\", \"fighter-champion\");\nawait moveFile(\"classes\", \"echo-knight\", \"fighter-echo-knight\");\nawait moveFile(\"classes\", \"eldritch-knight\", \"fighter-eldritch-knight\");\nawait moveFile(\"classes\", \"knight-ua\", \"fighter-knight-ua\");\nawait moveFile(\"classes\", \"monster-hunter-ua\", \"fighter-monster-hunter-ua\");\nawait moveFile(\"classes\", \"psi-warrior\", \"fighter-psi-warrior\");\nawait moveFile(\"classes\", \"purple-dragon-knight-banneret\", \"fighter-purple-dragon-knight-banneret\");\nawait moveFile(\"classes\", \"rune-knight\", \"fighter-rune-knight\");\nawait moveFile(\"classes\", \"samurai\", \"fighter-samurai\");\nawait moveFile(\"classes\", \"scout-ua\", \"fighter-scout-ua\");\nawait moveFile(\"classes\", \"sharpshooter-ua\", \"fighter-sharpshooter-ua\");\nawait moveFile(\"classes\", \"way-of-mercy\", \"monk-way-of-mercy\");\nawait moveFile(\"classes\", \"way-of-shadow\", \"monk-way-of-shadow\");\nawait moveFile(\"classes\", \"way-of-the-ascendant-dragon\", \"monk-way-of-the-ascendant-dragon\");\nawait moveFile(\"classes\", \"way-of-the-astral-self\", \"monk-way-of-the-astral-self\");\nawait moveFile(\"classes\", \"way-of-the-drunken-master\", \"monk-way-of-the-drunken-master\");\nawait moveFile(\"classes\", \"way-of-the-four-elements\", \"monk-way-of-the-four-elements\");\nawait moveFile(\"classes\", \"way-of-the-kensei\", \"monk-way-of-the-kensei\");\nawait moveFile(\"classes\", \"way-of-the-long-death\", \"monk-way-of-the-long-death\");\nawait moveFile(\"classes\", \"way-of-the-open-hand\", \"monk-way-of-the-open-hand\");\nawait moveFile(\"classes\", \"way-of-the-sun-soul\", \"monk-way-of-the-sun-soul\");\nawait moveFile(\"classes\", \"way-of-tranquility-ua\", \"monk-way-of-tranquility-ua\");\nawait moveFile(\"classes\", \"order-of-the-avatar\", \"mystic-order-of-the-avatar-uathemysticclass\");\nawait moveFile(\"classes\", \"order-of-the-awakened\", \"mystic-order-of-the-awakened-uathemysticclass\");\nawait moveFile(\"classes\", \"order-of-the-immortal\", \"mystic-order-of-the-immortal-uathemysticclass\");\nawait moveFile(\"classes\", \"order-of-the-nomad\", \"mystic-order-of-the-nomad-uathemysticclass\");\nawait moveFile(\"classes\", \"order-of-the-soul-knife\", \"mystic-order-of-the-soul-knife-uathemysticclass\");\nawait moveFile(\"classes\", \"order-of-the-wu-jen\", \"mystic-order-of-the-wu-jen-uathemysticclass\");\nawait moveFile(\"classes\", \"mystic-ua\", \"mystic-uathemysticclass\");\nawait moveFile(\"classes\", \"oath-of-conquest\", \"paladin-oath-of-conquest\");\nawait moveFile(\"classes\", \"oath-of-devotion\", \"paladin-oath-of-devotion\");\nawait moveFile(\"classes\", \"oath-of-glory\", \"paladin-oath-of-glory\");\nawait moveFile(\"classes\", \"oath-of-redemption\", \"paladin-oath-of-redemption\");\nawait moveFile(\"classes\", \"oath-of-the-ancients\", \"paladin-oath-of-the-ancients\");\nawait moveFile(\"classes\", \"oath-of-the-crown\", \"paladin-oath-of-the-crown\");\nawait moveFile(\"classes\", \"oath-of-the-watchers\", \"paladin-oath-of-the-watchers\");\nawait moveFile(\"classes\", \"oath-of-treachery-ua\", \"paladin-oath-of-treachery-ua\");\nawait moveFile(\"classes\", \"oath-of-vengeance\", \"paladin-oath-of-vengeance\");\nawait moveFile(\"classes\", \"oathbreaker\", \"paladin-oathbreaker\");\nawait moveFile(\"classes\", \"prestige-class-rune-scribe-ua\", \"prestige-class-rune-scribe-uaprestigeclassesrunmagic\");\nawait moveFile(\"classes\", \"ranger-ambuscade-ua\", \"ranger-ambuscade-uaranger\");\nawait moveFile(\"classes\", \"beast-master\", \"ranger-beast-master\");\nawait moveFile(\"classes\", \"drakewarden\", \"ranger-drakewarden\");\nawait moveFile(\"classes\", \"fey-wanderer\", \"ranger-fey-wanderer\");\nawait moveFile(\"classes\", \"gloom-stalker\", \"ranger-gloom-stalker\");\nawait moveFile(\"classes\", \"horizon-walker\", \"ranger-horizon-walker\");\nawait moveFile(\"classes\", \"hunter\", \"ranger-hunter\");\nawait moveFile(\"classes\", \"monster-slayer\", \"ranger-monster-slayer\");\nawait moveFile(\"classes\", \"primeval-guardian-ua\", \"ranger-primeval-guardian-ua\");\nawait moveFile(\"classes\", \"ranger-revised-ua\", \"ranger-revised-uatherangerrevised\");\nawait moveFile(\"classes\", \"ranger-spell-less-ua\", \"ranger-spell-less-uamodifyingclasses\");\nawait moveFile(\"classes\", \"swarmkeeper\", \"ranger-swarmkeeper\");\nawait moveFile(\"classes\", \"arcane-trickster\", \"rogue-arcane-trickster\");\nawait moveFile(\"classes\", \"assassin\", \"rogue-assassin\");\nawait moveFile(\"classes\", \"inquisitive\", \"rogue-inquisitive\");\nawait moveFile(\"classes\", \"mastermind\", \"rogue-mastermind\");\nawait moveFile(\"classes\", \"phantom\", \"rogue-phantom\");\nawait moveFile(\"classes\", \"scout\", \"rogue-scout\");\nawait moveFile(\"classes\", \"soulknife\", \"rogue-soulknife\");\nawait moveFile(\"classes\", \"swashbuckler\", \"rogue-swashbuckler\");\nawait moveFile(\"classes\", \"thief\", \"rogue-thief\");\nawait moveFile(\"classes\", \"aberrant-mind\", \"sorcerer-aberrant-mind\");\nawait moveFile(\"classes\", \"clockwork-soul\", \"sorcerer-clockwork-soul\");\nawait moveFile(\"classes\", \"divine-soul\", \"sorcerer-divine-soul\");\nawait moveFile(\"classes\", \"draconic-bloodline\", \"sorcerer-draconic-bloodline\");\nawait moveFile(\"classes\", \"giant-soul-ua\", \"sorcerer-giant-soul-ua\");\nawait moveFile(\"classes\", \"lunar-magic-ua\", \"sorcerer-lunar-magic-ua\");\nawait moveFile(\"classes\", \"phoenix-sorcery-ua\", \"sorcerer-phoenix-sorcery-ua\");\nawait moveFile(\"classes\", \"pyromancer-psk\", \"sorcerer-pyromancer-psk\");\nawait moveFile(\"classes\", \"sea-sorcery-ua\", \"sorcerer-sea-sorcery-ua\");\nawait moveFile(\"classes\", \"shadow-magic\", \"sorcerer-shadow-magic\");\nawait moveFile(\"classes\", \"stone-sorcery-ua\", \"sorcerer-stone-sorcery-ua\");\nawait moveFile(\"classes\", \"storm-sorcery\", \"sorcerer-storm-sorcery\");\nawait moveFile(\"classes\", \"wild-magic\", \"sorcerer-wild-magic\");\nawait moveFile(\"classes\", \"spellcaster-sidekick\", \"spellcaster-sidekick-tce\");\nawait moveFile(\"classes\", \"spellcaster-sidekick-ua\", \"spellcaster-sidekick-uasidekicks\");\nawait moveFile(\"classes\", \"ghost-in-the-machine-ua\", \"warlock-ghost-in-the-machine-ua\");\nawait moveFile(\"classes\", \"mage-of-witherbloom-ua\", \"warlock-mage-of-witherbloom-ua\");\nawait moveFile(\"classes\", \"the-archfey\", \"warlock-the-archfey\");\nawait moveFile(\"classes\", \"the-celestial\", \"warlock-the-celestial\");\nawait moveFile(\"classes\", \"the-fathomless\", \"warlock-the-fathomless\");\nawait moveFile(\"classes\", \"the-fiend\", \"warlock-the-fiend\");\nawait moveFile(\"classes\", \"the-genie\", \"warlock-the-genie\");\nawait moveFile(\"classes\", \"the-great-old-one\", \"warlock-the-great-old-one\");\nawait moveFile(\"classes\", \"the-hexblade\", \"warlock-the-hexblade\");\nawait moveFile(\"classes\", \"the-raven-queen-ua\", \"warlock-the-raven-queen-ua\");\nawait moveFile(\"classes\", \"the-seeker-ua\", \"warlock-the-seeker-ua\");\nawait moveFile(\"classes\", \"the-undead\", \"warlock-the-undead\");\nawait moveFile(\"classes\", \"the-undying\", \"warlock-the-undying\");\nawait moveFile(\"classes\", \"warrior-sidekick\", \"warrior-sidekick-tce\");\nawait moveFile(\"classes\", \"warrior-sidekick-ua\", \"warrior-sidekick-uasidekicks\");\nawait moveFile(\"classes\", \"artificer-ua\", \"wizard-artificer-ua\");\nawait moveFile(\"classes\", \"bladesinging\", \"wizard-bladesinging\");\nawait moveFile(\"classes\", \"chronurgy-magic\", \"wizard-chronurgy-magic\");\nawait moveFile(\"classes\", \"graviturgy-magic\", \"wizard-graviturgy-magic\");\nawait moveFile(\"classes\", \"lore-mastery-ua\", \"wizard-lore-mastery-ua\");\nawait moveFile(\"classes\", \"mage-of-prismari-ua\", \"wizard-mage-of-prismari-ua\");\nawait moveFile(\"classes\", \"mage-of-quandrix-ua\", \"wizard-mage-of-quandrix-ua\");\nawait moveFile(\"classes\", \"onomancy-ua\", \"wizard-onomancy-ua\");\nawait moveFile(\"classes\", \"order-of-scribes\", \"wizard-order-of-scribes\");\nawait moveFile(\"classes\", \"psionics-ua\", \"wizard-psionics-ua\");\nawait moveFile(\"classes\", \"runecrafter-ua\", \"wizard-runecrafter-ua\");\nawait moveFile(\"classes\", \"school-of-abjuration\", \"wizard-school-of-abjuration\");\nawait moveFile(\"classes\", \"school-of-conjuration\", \"wizard-school-of-conjuration\");\nawait moveFile(\"classes\", \"school-of-divination\", \"wizard-school-of-divination\");\nawait moveFile(\"classes\", \"school-of-enchantment\", \"wizard-school-of-enchantment\");\nawait moveFile(\"classes\", \"school-of-evocation\", \"wizard-school-of-evocation\");\nawait moveFile(\"classes\", \"school-of-illusion\", \"wizard-school-of-illusion\");\nawait moveFile(\"classes\", \"school-of-invention-ua\", \"wizard-school-of-invention-ua\");\nawait moveFile(\"classes\", \"school-of-necromancy\", \"wizard-school-of-necromancy\");\nawait moveFile(\"classes\", \"school-of-transmutation\", \"wizard-school-of-transmutation\");\nawait moveFile(\"classes\", \"technomancy-ua\", \"wizard-technomancy-ua\");\nawait moveFile(\"classes\", \"theurgy-ua\", \"wizard-theurgy-ua\");\nawait moveFile(\"classes\", \"war-magic\", \"wizard-war-magic\");\nawait moveFile(\"feats\", \"aberrant-dragonmark\", \"aberrant-dragonmark-erlw\");\nawait moveFile(\"feats\", \"acrobat-ua\", \"acrobat-uafeatsforskills\");\nawait moveFile(\"feats\", \"adept-of-the-black-robes-ua\", \"adept-of-the-black-robes-ua2022heroesofkrynnrevisited\");\nawait moveFile(\"feats\", \"adept-of-the-red-robes-ua\", \"adept-of-the-red-robes-ua2022heroesofkrynn\");\nawait moveFile(\"feats\", \"adept-of-the-white-robes-ua\", \"adept-of-the-white-robes-ua2022heroesofkrynn\");\nawait moveFile(\"feats\", \"agent-of-order-ua\", \"agent-of-order-ua2022wondersofthemultiverse\");\nawait moveFile(\"feats\", \"alchemist-ua\", \"alchemist-uafeats\");\nawait moveFile(\"feats\", \"animal-handler-ua\", \"animal-handler-uafeatsforskills\");\nawait moveFile(\"feats\", \"arcanist-ua\", \"arcanist-uafeatsforskills\");\nawait moveFile(\"feats\", \"artificer-initiate\", \"artificer-initiate-tce\");\nawait moveFile(\"feats\", \"artificer-initiate-ua\", \"artificer-initiate-ua2020feats\");\nawait moveFile(\"feats\", \"baleful-scion-ua\", \"baleful-scion-ua2022wondersofthemultiverse\");\nawait moveFile(\"feats\", \"barbed-hide-ua\", \"barbed-hide-uafeatsforraces\");\nawait moveFile(\"feats\", \"blade-mastery-ua\", \"blade-mastery-uafeats\");\nawait moveFile(\"feats\", \"bountiful-luck-ua\", \"bountiful-luck-uafeatsforraces\");\nawait moveFile(\"feats\", \"bountiful-luck\", \"bountiful-luck-xge\");\nawait moveFile(\"feats\", \"brawny-ua\", \"brawny-uafeatsforskills\");\nawait moveFile(\"feats\", \"burglar-ua\", \"burglar-uafeats\");\nawait moveFile(\"feats\", \"cartomancer-ua\", \"cartomancer-ua2022wondersofthemultiverse\");\nawait moveFile(\"feats\", \"chef\", \"chef-tce\");\nawait moveFile(\"feats\", \"chef-ua\", \"chef-ua2020feats\");\nawait moveFile(\"feats\", \"cohort-of-chaos-ua\", \"cohort-of-chaos-ua2022wondersofthemultiverse\");\nawait moveFile(\"feats\", \"critter-friend-ua\", \"critter-friend-uafeatsforraces\");\nawait moveFile(\"feats\", \"crusher\", \"crusher-tce\");\nawait moveFile(\"feats\", \"crusher-ua\", \"crusher-ua2020feats\");\nawait moveFile(\"feats\", \"diplomat-ua\", \"diplomat-uafeatsforskills\");\nawait moveFile(\"feats\", \"divine-communications-ua\", \"divine-communications-ua2022heroesofkrynn\");\nawait moveFile(\"feats\", \"divinely-favored-ua\", \"divinely-favored-ua2022heroesofkrynnrevisited\");\nawait moveFile(\"feats\", \"dragon-fear-ua\", \"dragon-fear-uafeatsforraces\");\nawait moveFile(\"feats\", \"dragon-fear\", \"dragon-fear-xge\");\nawait moveFile(\"feats\", \"dragon-hide-ua\", \"dragon-hide-uafeatsforraces\");\nawait moveFile(\"feats\", \"dragon-hide\", \"dragon-hide-xge\");\nawait moveFile(\"feats\", \"dragon-wings-ua\", \"dragon-wings-uafeatsforraces\");\nawait moveFile(\"feats\", \"dragonmark-of-detection-ua\", \"dragonmark-of-detection-uaeberron\");\nawait moveFile(\"feats\", \"dragonmark-of-finding-ua\", \"dragonmark-of-finding-uaeberron\");\nawait moveFile(\"feats\", \"dragonmark-of-handling-ua\", \"dragonmark-of-handling-uaeberron\");\nawait moveFile(\"feats\", \"dragonmark-of-healing-ua\", \"dragonmark-of-healing-uaeberron\");\nawait moveFile(\"feats\", \"dragonmark-of-hospitality-ua\", \"dragonmark-of-hospitality-uaeberron\");\nawait moveFile(\"feats\", \"dragonmark-of-making-ua\", \"dragonmark-of-making-uaeberron\");\nawait moveFile(\"feats\", \"dragonmark-of-passage-ua\", \"dragonmark-of-passage-uaeberron\");\nawait moveFile(\"feats\", \"dragonmark-of-scribing-ua\", \"dragonmark-of-scribing-uaeberron\");\nawait moveFile(\"feats\", \"dragonmark-of-sentinel-ua\", \"dragonmark-of-sentinel-uaeberron\");\nawait moveFile(\"feats\", \"dragonmark-of-shadow-ua\", \"dragonmark-of-shadow-uaeberron\");\nawait moveFile(\"feats\", \"dragonmark-of-storm-ua\", \"dragonmark-of-storm-uaeberron\");\nawait moveFile(\"feats\", \"dragonmark-of-warding-ua\", \"dragonmark-of-warding-uaeberron\");\nawait moveFile(\"feats\", \"drow-high-magic-ua\", \"drow-high-magic-uafeatsforraces\");\nawait moveFile(\"feats\", \"drow-high-magic\", \"drow-high-magic-xge\");\nawait moveFile(\"feats\", \"dwarf-resilience-ua\", \"dwarf-resilience-uafeatsforraces\");\nawait moveFile(\"feats\", \"dwarven-fortitude\", \"dwarven-fortitude-xge\");\nawait moveFile(\"feats\", \"eldritch-adept\", \"eldritch-adept-tce\");\nawait moveFile(\"feats\", \"eldritch-adept-ua\", \"eldritch-adept-ua2020feats\");\nawait moveFile(\"feats\", \"elemental-touched-ua\", \"elemental-touched-ua2022giantoptions\");\nawait moveFile(\"feats\", \"elven-accuracy-ua\", \"elven-accuracy-uafeatsforraces\");\nawait moveFile(\"feats\", \"elven-accuracy\", \"elven-accuracy-xge\");\nawait moveFile(\"feats\", \"ember-of-the-fire-giant-ua\", \"ember-of-the-fire-giant-ua2022wondersofthemultiverse\");\nawait moveFile(\"feats\", \"empathic-ua\", \"empathic-uafeatsforskills\");\nawait moveFile(\"feats\", \"everybodys-friend-ua\", \"everybodys-friend-uafeatsforraces\");\nawait moveFile(\"feats\", \"fade-away-ua\", \"fade-away-uafeatsforraces\");\nawait moveFile(\"feats\", \"fade-away\", \"fade-away-xge\");\nawait moveFile(\"feats\", \"fell-handed-ua\", \"fell-handed-uafeats\");\nawait moveFile(\"feats\", \"fey-teleportation-ua\", \"fey-teleportation-uafeatsforraces\");\nawait moveFile(\"feats\", \"fey-teleportation\", \"fey-teleportation-xge\");\nawait moveFile(\"feats\", \"fey-touched\", \"fey-touched-tce\");\nawait moveFile(\"feats\", \"fey-touched-ua\", \"fey-touched-ua2020feats\");\nawait moveFile(\"feats\", \"fighting-initiate\", \"fighting-initiate-tce\");\nawait moveFile(\"feats\", \"fighting-initiate-ua\", \"fighting-initiate-ua2020feats\");\nawait moveFile(\"feats\", \"flail-mastery-ua\", \"flail-mastery-uafeats\");\nawait moveFile(\"feats\", \"flames-of-phlegethos-ua\", \"flames-of-phlegethos-uafeatsforraces\");\nawait moveFile(\"feats\", \"flames-of-phlegethos\", \"flames-of-phlegethos-xge\");\nawait moveFile(\"feats\", \"fury-of-the-frost-giant-ua\", \"fury-of-the-frost-giant-ua2022wondersofthemultiverse\");\nawait moveFile(\"feats\", \"gift-of-the-chromatic-dragon\", \"gift-of-the-chromatic-dragon-ftd\");\nawait moveFile(\"feats\", \"gift-of-the-chromatic-dragon-ua\", \"gift-of-the-chromatic-dragon-ua2021draconicoptions\");\nawait moveFile(\"feats\", \"gift-of-the-gem-dragon\", \"gift-of-the-gem-dragon-ftd\");\nawait moveFile(\"feats\", \"gift-of-the-gem-dragon-ua\", \"gift-of-the-gem-dragon-ua2021draconicoptions\");\nawait moveFile(\"feats\", \"gift-of-the-metallic-dragon\", \"gift-of-the-metallic-dragon-ftd\");\nawait moveFile(\"feats\", \"gift-of-the-metallic-dragon-ua\", \"gift-of-the-metallic-dragon-ua2021draconicoptions\");\nawait moveFile(\"feats\", \"gourmand-ua\", \"gourmand-uafeats\");\nawait moveFile(\"feats\", \"grudge-bearer-ua\", \"grudge-bearer-uafeatsforraces\");\nawait moveFile(\"feats\", \"guile-of-the-cloud-giant-ua\", \"guile-of-the-cloud-giant-ua2022wondersofthemultiverse\");\nawait moveFile(\"feats\", \"gunner\", \"gunner-tce\");\nawait moveFile(\"feats\", \"gunner-ua\", \"gunner-ua2020feats\");\nawait moveFile(\"feats\", \"historian-ua\", \"historian-uafeatsforskills\");\nawait moveFile(\"feats\", \"human-determination-ua\", \"human-determination-uafeatsforraces\");\nawait moveFile(\"feats\", \"infernal-constitution-ua\", \"infernal-constitution-uafeatsforraces\");\nawait moveFile(\"feats\", \"infernal-constitution\", \"infernal-constitution-xge\");\nawait moveFile(\"feats\", \"initiate-of-high-sorcery-ua\", \"initiate-of-high-sorcery-ua2022heroesofkrynnrevisited\");\nawait moveFile(\"feats\", \"investigator-ua\", \"investigator-uafeatsforskills\");\nawait moveFile(\"feats\", \"keenness-of-the-stone-giant-ua\", \"keenness-of-the-stone-giant-ua2022giantoptions\");\nawait moveFile(\"feats\", \"knight-of-the-crown-ua\", \"knight-of-the-crown-ua2022heroesofkrynn\");\nawait moveFile(\"feats\", \"knight-of-the-rose-ua\", \"knight-of-the-rose-ua2022heroesofkrynnrevisited\");\nawait moveFile(\"feats\", \"knight-of-the-sword-ua\", \"knight-of-the-sword-ua2022heroesofkrynnrevisited\");\nawait moveFile(\"feats\", \"master-of-disguise-ua\", \"master-of-disguise-uafeats\");\nawait moveFile(\"feats\", \"medic-ua\", \"medic-uafeatsforskills\");\nawait moveFile(\"feats\", \"menacing-ua\", \"menacing-uafeatsforskills\");\nawait moveFile(\"feats\", \"metabolic-control-ua\", \"metabolic-control-ua2020psionicoptionsrevisited\");\nawait moveFile(\"feats\", \"metamagic-adept\", \"metamagic-adept-tce\");\nawait moveFile(\"feats\", \"metamagic-adept-ua\", \"metamagic-adept-ua2020feats\");\nawait moveFile(\"feats\", \"naturalist-ua\", \"naturalist-uafeatsforskills\");\nawait moveFile(\"feats\", \"orcish-aggression-ua\", \"orcish-aggression-uafeatsforraces\");\nawait moveFile(\"feats\", \"orcish-fury-ua\", \"orcish-fury-uafeatsforraces\");\nawait moveFile(\"feats\", \"orcish-fury\", \"orcish-fury-xge\");\nawait moveFile(\"feats\", \"outlands-envoy-ua\", \"outlands-envoy-ua2022wondersofthemultiverse\");\nawait moveFile(\"feats\", \"outsized-might-ua\", \"outsized-might-ua2022giantoptions\");\nawait moveFile(\"feats\", \"perceptive-ua\", \"perceptive-uafeatsforskills\");\nawait moveFile(\"feats\", \"performer-ua\", \"performer-uafeatsforskills\");\nawait moveFile(\"feats\", \"piercer\", \"piercer-tce\");\nawait moveFile(\"feats\", \"piercer-ua\", \"piercer-ua2020feats\");\nawait moveFile(\"feats\", \"planar-wanderer-ua\", \"planar-wanderer-ua2022wondersofthemultiverse\");\nawait moveFile(\"feats\", \"poisoner\", \"poisoner-tce\");\nawait moveFile(\"feats\", \"poisoner-ua\", \"poisoner-ua2020feats\");\nawait moveFile(\"feats\", \"practiced-expert-ua\", \"practiced-expert-ua2020feats\");\nawait moveFile(\"feats\", \"prodigy-ua\", \"prodigy-uafeatsforraces\");\nawait moveFile(\"feats\", \"prodigy\", \"prodigy-xge\");\nawait moveFile(\"feats\", \"quick-fingered-ua\", \"quick-fingered-uafeatsforskills\");\nawait moveFile(\"feats\", \"quicksmithing\", \"quicksmithing-psk\");\nawait moveFile(\"feats\", \"revenant-blade\", \"revenant-blade-erlw\");\nawait moveFile(\"feats\", \"righteous-heritor-ua\", \"righteous-heritor-ua2022wondersofthemultiverse\");\nawait moveFile(\"feats\", \"rune-carver-adept-ua\", \"rune-carver-adept-ua2022wondersofthemultiverse\");\nawait moveFile(\"feats\", \"rune-carver-apprentice-ua\", \"rune-carver-apprentice-ua2022wondersofthemultiverse\");\nawait moveFile(\"feats\", \"scion-of-elemental-air-ua\", \"scion-of-elemental-air-ua2022wondersofthemultiverse\");\nawait moveFile(\"feats\", \"scion-of-elemental-earth-ua\", \"scion-of-elemental-earth-ua2022wondersofthemultiverse\");\nawait moveFile(\"feats\", \"scion-of-elemental-fire-ua\", \"scion-of-elemental-fire-ua2022wondersofthemultiverse\");\nawait moveFile(\"feats\", \"scion-of-elemental-water-ua\", \"scion-of-elemental-water-ua2022wondersofthemultiverse\");\nawait moveFile(\"feats\", \"scion-of-the-outer-planes-ua\", \"scion-of-the-outer-planes-ua2022wondersofthemultiverse\");\nawait moveFile(\"feats\", \"second-chance-ua\", \"second-chance-uafeatsforraces\");\nawait moveFile(\"feats\", \"second-chance\", \"second-chance-xge\");\nawait moveFile(\"feats\", \"servo-crafting\", \"servo-crafting-psk\");\nawait moveFile(\"feats\", \"shadow-touched\", \"shadow-touched-tce\");\nawait moveFile(\"feats\", \"shadow-touched-ua\", \"shadow-touched-ua2020feats\");\nawait moveFile(\"feats\", \"shield-training-ua\", \"shield-training-ua2020feats\");\nawait moveFile(\"feats\", \"silver-tongued-ua\", \"silver-tongued-uafeatsforskills\");\nawait moveFile(\"feats\", \"skill-expert\", \"skill-expert-tce\");\nawait moveFile(\"feats\", \"slasher\", \"slasher-tce\");\nawait moveFile(\"feats\", \"slasher-ua\", \"slasher-ua2020feats\");\nawait moveFile(\"feats\", \"soul-of-the-storm-giant-ua\", \"soul-of-the-storm-giant-ua2022giantoptions\");\nawait moveFile(\"feats\", \"spear-mastery-ua\", \"spear-mastery-uafeats\");\nawait moveFile(\"feats\", \"squat-nimbleness-ua\", \"squat-nimbleness-uafeatsforraces\");\nawait moveFile(\"feats\", \"squat-nimbleness\", \"squat-nimbleness-xge\");\nawait moveFile(\"feats\", \"squire-of-solamnia-ua\", \"squire-of-solamnia-ua2022heroesofkrynn\");\nawait moveFile(\"feats\", \"stealthy-ua\", \"stealthy-uafeatsforskills\");\nawait moveFile(\"feats\", \"strike-of-the-giants-ua\", \"strike-of-the-giants-ua2022wondersofthemultiverse\");\nawait moveFile(\"feats\", \"strixhaven-initiate\", \"strixhaven-initiate-scc\");\nawait moveFile(\"feats\", \"strixhaven-mascot\", \"strixhaven-mascot-scc\");\nawait moveFile(\"feats\", \"survivalist-ua\", \"survivalist-uafeatsforskills\");\nawait moveFile(\"feats\", \"svirfneblin-magic\", \"svirfneblin-magic-mtf\");\nawait moveFile(\"feats\", \"tandem-tactician-ua\", \"tandem-tactician-ua2020feats\");\nawait moveFile(\"feats\", \"telekinetic\", \"telekinetic-tce\");\nawait moveFile(\"feats\", \"telekinetic-ua\", \"telekinetic-uafighterroguewizard\");\nawait moveFile(\"feats\", \"telepathic\", \"telepathic-tce\");\nawait moveFile(\"feats\", \"telepathic-ua\", \"telepathic-ua2020psionicoptionsrevisited\");\nawait moveFile(\"feats\", \"theologian-ua\", \"theologian-uafeatsforskills\");\nawait moveFile(\"feats\", \"tower-of-iron-will-ua\", \"tower-of-iron-will-ua2020psionicoptionsrevisited\");\nawait moveFile(\"feats\", \"tracker-ua\", \"tracker-ua2020feats\");\nawait moveFile(\"feats\", \"vampiric-exultation\", \"vampiric-exultation-psx\");\nawait moveFile(\"feats\", \"vigor-of-the-hill-giant-ua\", \"vigor-of-the-hill-giant-ua2022wondersofthemultiverse\");\nawait moveFile(\"feats\", \"warhammer-master-ua\", \"warhammer-master-uafeats\");\nawait moveFile(\"feats\", \"wild-talent-ua\", \"wild-talent-ua2020psionicoptionsrevisited\");\nawait moveFile(\"feats\", \"wonder-maker-ua\", \"wonder-maker-uafeatsforraces\");\nawait moveFile(\"feats\", \"wood-elf-magic-ua\", \"wood-elf-magic-uafeatsforraces\");\nawait moveFile(\"feats\", \"wood-elf-magic\", \"wood-elf-magic-xge\");\nawait moveFile(\"items\", \"1-all-purpose-tool\", \"1-all-purpose-tool-tce\");\nawait moveFile(\"items\", \"1-amulet-of-the-devout\", \"1-amulet-of-the-devout-tce\");\nawait moveFile(\"items\", \"1-arcane-grimoire\", \"1-arcane-grimoire-tce\");\nawait moveFile(\"items\", \"1-bloodwell-vial\", \"1-bloodwell-vial-tce\");\nawait moveFile(\"items\", \"1-dragonhide-belt\", \"1-dragonhide-belt-ftd\");\nawait moveFile(\"items\", \"1-moon-sickle\", \"1-moon-sickle-tce\");\nawait moveFile(\"items\", \"1-rhythm-makers-drum\", \"1-rhythm-makers-drum-tce\");\nawait moveFile(\"items\", \"2-all-purpose-tool\", \"2-all-purpose-tool-tce\");\nawait moveFile(\"items\", \"2-amulet-of-the-devout\", \"2-amulet-of-the-devout-tce\");\nawait moveFile(\"items\", \"2-arcane-grimoire\", \"2-arcane-grimoire-tce\");\nawait moveFile(\"items\", \"2-bloodwell-vial\", \"2-bloodwell-vial-tce\");\nawait moveFile(\"items\", \"2-dragonhide-belt\", \"2-dragonhide-belt-ftd\");\nawait moveFile(\"items\", \"2-moon-sickle\", \"2-moon-sickle-tce\");\nawait moveFile(\"items\", \"2-rhythm-makers-drum\", \"2-rhythm-makers-drum-tce\");\nawait moveFile(\"items\", \"3-all-purpose-tool\", \"3-all-purpose-tool-tce\");\nawait moveFile(\"items\", \"3-amulet-of-the-devout\", \"3-amulet-of-the-devout-tce\");\nawait moveFile(\"items\", \"3-arcane-grimoire\", \"3-arcane-grimoire-tce\");\nawait moveFile(\"items\", \"3-bloodwell-vial\", \"3-bloodwell-vial-tce\");\nawait moveFile(\"items\", \"3-dragonhide-belt\", \"3-dragonhide-belt-ftd\");\nawait moveFile(\"items\", \"3-moon-sickle\", \"3-moon-sickle-tce\");\nawait moveFile(\"items\", \"3-rhythm-makers-drum\", \"3-rhythm-makers-drum-tce\");\nawait moveFile(\"items\", \"abracadabrus\", \"abracadabrus-idrotf\");\nawait moveFile(\"items\", \"absorbing-tattoo\", \"absorbing-tattoo-ua2020spellsandmagictattoos\");\nawait moveFile(\"items\", \"acid-absorbing-tattoo\", \"acid-absorbing-tattoo-tce\");\nawait moveFile(\"items\", \"adamantine-bar\", \"adamantine-bar-wdh\");\nawait moveFile(\"items\", \"adjustable-stilts\", \"adjustable-stilts-wdh\");\nawait moveFile(\"items\", \"akmon-hammer-of-purphoros\", \"akmon-hammer-of-purphoros-mot\");\nawait moveFile(\"items\", \"alchemical-compendium\", \"alchemical-compendium-tce\");\nawait moveFile(\"items\", \"alchemists-doom\", \"alchemists-doom-scc\");\nawait moveFile(\"items\", \"alchemy-jug-blue\", \"alchemy-jug-blue-cm\");\nawait moveFile(\"items\", \"alchemy-jug-orange\", \"alchemy-jug-orange-cm\");\nawait moveFile(\"items\", \"amber-runestone\", \"amber-runestone-nrh-tlt\");\nawait moveFile(\"items\", \"amethyst-lodestone\", \"amethyst-lodestone-ftd\");\nawait moveFile(\"items\", \"amulet-of-dinosaur-feathers-sun-empire\", \"amulet-of-dinosaur-feathers-sun-empire-psx\");\nawait moveFile(\"items\", \"amulet-of-harmony\", \"amulet-of-harmony-nrh-at\");\nawait moveFile(\"items\", \"amulet-of-protection-from-turning\", \"amulet-of-protection-from-turning-tftyp\");\nawait moveFile(\"items\", \"amulet-of-the-black-skull\", \"amulet-of-the-black-skull-toa\");\nawait moveFile(\"items\", \"amulet-of-the-drunkard\", \"amulet-of-the-drunkard-egw\");\nawait moveFile(\"items\", \"arcanaloths-music-box\", \"arcanaloths-music-box-toa\");\nawait moveFile(\"items\", \"arcane-cannon\", \"arcane-cannon-egw\");\nawait moveFile(\"items\", \"arcane-propulsion-arm\", \"arcane-propulsion-arm-erlw\");\nawait moveFile(\"items\", \"ascendant-dragon-touched-focus\", \"ascendant-dragon-touched-focus-ftd\");\nawait moveFile(\"items\", \"ascendant-dragon-vessel\", \"ascendant-dragon-vessel-ftd\");\nawait moveFile(\"items\", \"ascendant-scaled-ornament\", \"ascendant-scaled-ornament-ftd\");\nawait moveFile(\"items\", \"astral-shard\", \"astral-shard-tce\");\nawait moveFile(\"items\", \"astromancy-archive\", \"astromancy-archive-tce\");\nawait moveFile(\"items\", \"atlas-of-endless-horizons\", \"atlas-of-endless-horizons-tce\");\nawait moveFile(\"items\", \"axe-beak\", \"axe-beak-idrotf\");\nawait moveFile(\"items\", \"azorius-guild-signet\", \"azorius-guild-signet-ggr\");\nawait moveFile(\"items\", \"azorius-keyrune\", \"azorius-keyrune-ggr\");\nawait moveFile(\"items\", \"azuredge\", \"azuredge-wdh\");\nawait moveFile(\"items\", \"baba-yagas-mortar-and-pestle\", \"baba-yagas-mortar-and-pestle-tce\");\nawait moveFile(\"items\", \"baba-yagas-pestle\", \"baba-yagas-pestle-tce\");\nawait moveFile(\"items\", \"backpack-parachute\", \"backpack-parachute-wdh\");\nawait moveFile(\"items\", \"badge-of-the-watch\", \"badge-of-the-watch-wdh\");\nawait moveFile(\"items\", \"bag-of-bounty\", \"bag-of-bounty-uawge\");\nawait moveFile(\"items\", \"balance-of-harmony\", \"balance-of-harmony-tftyp\");\nawait moveFile(\"items\", \"balloon-pack\", \"balloon-pack-pota\");\nawait moveFile(\"items\", \"band-of-loyalty\", \"band-of-loyalty-uawge\");\nawait moveFile(\"items\", \"banner-of-the-krig-rune\", \"banner-of-the-krig-rune-skt\");\nawait moveFile(\"items\", \"barking-box\", \"barking-box-wdh\");\nawait moveFile(\"items\", \"barrier-peaks-trinket\", \"barrier-peaks-trinket-llk\");\nawait moveFile(\"items\", \"barrier-tattoo-large\", \"barrier-tattoo-large-tce\");\nawait moveFile(\"items\", \"barrier-tattoo-medium\", \"barrier-tattoo-medium-tce\");\nawait moveFile(\"items\", \"barrier-tattoo-rare\", \"barrier-tattoo-rare-ua2020spellsandmagictattoos\");\nawait moveFile(\"items\", \"barrier-tattoo-small\", \"barrier-tattoo-small-tce\");\nawait moveFile(\"items\", \"barrier-tattoo-uncommon\", \"barrier-tattoo-uncommon-ua2020spellsandmagictattoos\");\nawait moveFile(\"items\", \"barrier-tattoo-very-rare\", \"barrier-tattoo-very-rare-ua2020spellsandmagictattoos\");\nawait moveFile(\"items\", \"basic-fishing-equipment\", \"basic-fishing-equipment-aag\");\nawait moveFile(\"items\", \"battering-shield\", \"battering-shield-egw\");\nawait moveFile(\"items\", \"battle-standard-of-infernal-power\", \"battle-standard-of-infernal-power-bgdia\");\nawait moveFile(\"items\", \"bead-of-nourishment\", \"bead-of-nourishment-xge\");\nawait moveFile(\"items\", \"bead-of-refreshment\", \"bead-of-refreshment-xge\");\nawait moveFile(\"items\", \"bejeweled-ivory-drinking-horn-with-gold-inlay-brazen-coalition\", \"bejeweled-ivory-drinking-horn-with-gold-inlay-brazen-coalition-psx\");\nawait moveFile(\"items\", \"belashyrras-beholder-crown\", \"belashyrras-beholder-crown-erlw\");\nawait moveFile(\"items\", \"bell-branch\", \"bell-branch-tce\");\nawait moveFile(\"items\", \"birdpipes\", \"birdpipes-scag\");\nawait moveFile(\"items\", \"bizas-breath\", \"bizas-breath-jttrc\");\nawait moveFile(\"items\", \"black-chromatic-rose\", \"black-chromatic-rose-wbtw\");\nawait moveFile(\"items\", \"black-crystal-tablet\", \"black-crystal-tablet-wdmm\");\nawait moveFile(\"items\", \"black-dragon-mask\", \"black-dragon-mask-hotdq\");\nawait moveFile(\"items\", \"black-ghost-orchid-seed\", \"black-ghost-orchid-seed-jttrc\");\nawait moveFile(\"items\", \"black-sap\", \"black-sap-egw\");\nawait moveFile(\"items\", \"blackstaff\", \"blackstaff-wdh\");\nawait moveFile(\"items\", \"blade-of-avernus\", \"blade-of-avernus-bgdia\");\nawait moveFile(\"items\", \"blade-of-broken-mirrors-awakened\", \"blade-of-broken-mirrors-awakened-egw\");\nawait moveFile(\"items\", \"blade-of-broken-mirrors-dormant\", \"blade-of-broken-mirrors-dormant-egw\");\nawait moveFile(\"items\", \"blade-of-broken-mirrors-exalted\", \"blade-of-broken-mirrors-exalted-egw\");\nawait moveFile(\"items\", \"blast-scepter\", \"blast-scepter-wdmm\");\nawait moveFile(\"items\", \"blasting-powder\", \"blasting-powder-egw\");\nawait moveFile(\"items\", \"blight-ichor\", \"blight-ichor-egw\");\nawait moveFile(\"items\", \"blod-stone\", \"blod-stone-skt\");\nawait moveFile(\"items\", \"blood-fury-tattoo\", \"blood-fury-tattoo-ua2020spellsandmagictattoos\");\nawait moveFile(\"items\", \"blood-of-the-lycanthrope-antidote\", \"blood-of-the-lycanthrope-antidote-imr\");\nawait moveFile(\"items\", \"blood-of-the-lycanthrope\", \"blood-of-the-lycanthrope-imr\");\nawait moveFile(\"items\", \"blood-spear\", \"blood-spear-cos\");\nawait moveFile(\"items\", \"bloodaxe\", \"bloodaxe-egw\");\nawait moveFile(\"items\", \"blue-chromatic-rose\", \"blue-chromatic-rose-wbtw\");\nawait moveFile(\"items\", \"blue-dragon-mask\", \"blue-dragon-mask-rotos\");\nawait moveFile(\"items\", \"bob\", \"bob-toa\");\nawait moveFile(\"items\", \"bobbing-lily-pad\", \"bobbing-lily-pad-wbtw\");\nawait moveFile(\"items\", \"bombard\", \"bombard-aag\");\nawait moveFile(\"items\", \"bonecounter\", \"bonecounter-sdw\");\nawait moveFile(\"items\", \"bookmark\", \"bookmark-toa\");\nawait moveFile(\"items\", \"boots-of-false-tracks\", \"boots-of-false-tracks-xge\");\nawait moveFile(\"items\", \"boros-guild-signet\", \"boros-guild-signet-ggr\");\nawait moveFile(\"items\", \"boros-keyrune\", \"boros-keyrune-ggr\");\nawait moveFile(\"items\", \"bottle-of-boundless-coffee\", \"bottle-of-boundless-coffee-scc\");\nawait moveFile(\"items\", \"bottle-of-witchlight-wine\", \"bottle-of-witchlight-wine-wbtw\");\nawait moveFile(\"items\", \"bottled-breath\", \"bottled-breath-pota\");\nawait moveFile(\"items\", \"bracelet-of-rock-magic\", \"bracelet-of-rock-magic-tftyp\");\nawait moveFile(\"items\", \"bracer-of-flying-daggers\", \"bracer-of-flying-daggers-wdh\");\nawait moveFile(\"items\", \"breathing-bubble\", \"breathing-bubble-egw\");\nawait moveFile(\"items\", \"bridle-of-capturing\", \"bridle-of-capturing-imr\");\nawait moveFile(\"items\", \"bronze-spyglass-brazen-coalition\", \"bronze-spyglass-brazen-coalition-psx\");\nawait moveFile(\"items\", \"brooch-of-living-essence\", \"brooch-of-living-essence-egw\");\nawait moveFile(\"items\", \"butchers-bib\", \"butchers-bib-egw\");\nawait moveFile(\"items\", \"candle-mace\", \"candle-mace-bgdia\");\nawait moveFile(\"items\", \"candle-of-the-deep\", \"candle-of-the-deep-xge\");\nawait moveFile(\"items\", \"canoe\", \"canoe-toa\");\nawait moveFile(\"items\", \"cartographers-map-case\", \"cartographers-map-case-ai\");\nawait moveFile(\"items\", \"carved-jade-statuette-river-heralds\", \"carved-jade-statuette-river-heralds-psx\");\nawait moveFile(\"items\", \"catapult-munition\", \"catapult-munition-scc\");\nawait moveFile(\"items\", \"cauldron-of-plenty\", \"cauldron-of-plenty-idrotf\");\nawait moveFile(\"items\", \"cauldron-of-rebirth\", \"cauldron-of-rebirth-tce\");\nawait moveFile(\"items\", \"ceremonial-silver-dagger-with-gold-pommel-and-black-pearl-legion-of-dusk\", \"ceremonial-silver-dagger-with-gold-pommel-and-black-pearl-legion-of-dusk-psx\");\nawait moveFile(\"items\", \"charlatans-die\", \"charlatans-die-xge\");\nawait moveFile(\"items\", \"charm-of-the-monarch\", \"charm-of-the-monarch-wbtw\");\nawait moveFile(\"items\", \"charm-of-plant-command\", \"charm-of-plant-command-gos\");\nawait moveFile(\"items\", \"charred-wand-of-magic-missiles\", \"charred-wand-of-magic-missiles-wdh\");\nawait moveFile(\"items\", \"chest-of-preserving\", \"chest-of-preserving-wdmm\");\nawait moveFile(\"items\", \"chip-of-creation\", \"chip-of-creation-aitfr-avt\");\nawait moveFile(\"items\", \"chronolometer\", \"chronolometer-ai\");\nawait moveFile(\"items\", \"circlet-of-human-perfection\", \"circlet-of-human-perfection-wdmm\");\nawait moveFile(\"items\", \"claw-of-the-wyrm-rune\", \"claw-of-the-wyrm-rune-skt\");\nawait moveFile(\"items\", \"claws-of-the-umber-hulk\", \"claws-of-the-umber-hulk-pota\");\nawait moveFile(\"items\", \"cleansing-stone\", \"cleansing-stone-erlw\");\nawait moveFile(\"items\", \"cloak-of-billowing\", \"cloak-of-billowing-xge\");\nawait moveFile(\"items\", \"cloak-of-many-fashions\", \"cloak-of-many-fashions-xge\");\nawait moveFile(\"items\", \"clockwork-amulet\", \"clockwork-amulet-xge\");\nawait moveFile(\"items\", \"clockwork-dog\", \"clockwork-dog-skt\");\nawait moveFile(\"items\", \"cloth-of-gold-vestments-legion-of-dusk\", \"cloth-of-gold-vestments-legion-of-dusk-psx\");\nawait moveFile(\"items\", \"clothes-of-mending\", \"clothes-of-mending-xge\");\nawait moveFile(\"items\", \"clothing-cold-weather\", \"clothing-cold-weather-idrotf\");\nawait moveFile(\"items\", \"coiling-grasp-tattoo\", \"coiling-grasp-tattoo-tce\");\nawait moveFile(\"items\", \"coin-of-decisionry\", \"coin-of-decisionry-ai\");\nawait moveFile(\"items\", \"coin-of-delving\", \"coin-of-delving-egw\");\nawait moveFile(\"items\", \"cold-absorbing-tattoo\", \"cold-absorbing-tattoo-tce\");\nawait moveFile(\"items\", \"common-glamerweave\", \"common-glamerweave-erlw\");\nawait moveFile(\"items\", \"concertina\", \"concertina-rmbre\");\nawait moveFile(\"items\", \"conch-of-safe-rest\", \"conch-of-safe-rest-ttp\");\nawait moveFile(\"items\", \"conch-of-teleportation\", \"conch-of-teleportation-skt\");\nawait moveFile(\"items\", \"copper-alms-coin\", \"copper-alms-coin-ggr\");\nawait moveFile(\"items\", \"copper-stein-with-silver-filigree-brazen-coalition\", \"copper-stein-with-silver-filigree-brazen-coalition-psx\");\nawait moveFile(\"items\", \"copper-zib\", \"copper-zib-ggr\");\nawait moveFile(\"items\", \"cracked-driftglobe\", \"cracked-driftglobe-cm\");\nawait moveFile(\"items\", \"crampons\", \"crampons-idrotf\");\nawait moveFile(\"items\", \"crook-of-rao\", \"crook-of-rao-tce\");\nawait moveFile(\"items\", \"crown-of-the-forest\", \"crown-of-the-forest-imr\");\nawait moveFile(\"items\", \"crystalline-chronicle\", \"crystalline-chronicle-tce\");\nawait moveFile(\"items\", \"cuddly-strixhaven-mascot\", \"cuddly-strixhaven-mascot-scc\");\nawait moveFile(\"items\", \"cuddly-toy-spider\", \"cuddly-toy-spider-wbtw\");\nawait moveFile(\"items\", \"cursed-luckstone\", \"cursed-luckstone-gos\");\nawait moveFile(\"items\", \"dagger-of-blindsight\", \"dagger-of-blindsight-wdmm\");\nawait moveFile(\"items\", \"dagger-of-guitar-solos\", \"dagger-of-guitar-solos-wdmm\");\nawait moveFile(\"items\", \"damselfly-ship\", \"damselfly-ship-aag\");\nawait moveFile(\"items\", \"dancing-monkey-fruit\", \"dancing-monkey-fruit-toa\");\nawait moveFile(\"items\", \"danoths-visor-awakened\", \"danoths-visor-awakened-egw\");\nawait moveFile(\"items\", \"danoths-visor-dormant\", \"danoths-visor-dormant-egw\");\nawait moveFile(\"items\", \"danoths-visor-exalted\", \"danoths-visor-exalted-egw\");\nawait moveFile(\"items\", \"dark-shard-amulet\", \"dark-shard-amulet-xge\");\nawait moveFile(\"items\", \"dawnbringer\", \"dawnbringer-oota\");\nawait moveFile(\"items\", \"deck-of-several-things\", \"deck-of-several-things-llk\");\nawait moveFile(\"items\", \"dekella-bident-of-thassa\", \"dekella-bident-of-thassa-mot\");\nawait moveFile(\"items\", \"demonomicon-of-iggwilv\", \"demonomicon-of-iggwilv-tce\");\nawait moveFile(\"items\", \"devastation-orb-of-air\", \"devastation-orb-of-air-pota\");\nawait moveFile(\"items\", \"devastation-orb-of-earth\", \"devastation-orb-of-earth-pota\");\nawait moveFile(\"items\", \"devastation-orb-of-fire\", \"devastation-orb-of-fire-pota\");\nawait moveFile(\"items\", \"devastation-orb-of-water\", \"devastation-orb-of-water-pota\");\nawait moveFile(\"items\", \"devlins-staff-of-striking\", \"devlins-staff-of-striking-toa\");\nawait moveFile(\"items\", \"devotees-censer\", \"devotees-censer-tce\");\nawait moveFile(\"items\", \"dimensional-loop\", \"dimensional-loop-ai\");\nawait moveFile(\"items\", \"dimir-guild-signet\", \"dimir-guild-signet-ggr\");\nawait moveFile(\"items\", \"dimir-keyrune\", \"dimir-keyrune-ggr\");\nawait moveFile(\"items\", \"dispelling-stone\", \"dispelling-stone-egw\");\nawait moveFile(\"items\", \"docent\", \"docent-erlw\");\nawait moveFile(\"items\", \"documancy-satchel\", \"documancy-satchel-ai\");\nawait moveFile(\"items\", \"dodecahedron-of-doom\", \"dodecahedron-of-doom-wdmm\");\nawait moveFile(\"items\", \"dogsled\", \"dogsled-idrotf\");\nawait moveFile(\"items\", \"double-bladed-scimitar\", \"double-bladed-scimitar-erlw\");\nawait moveFile(\"items\", \"dowsing-dagger\", \"dowsing-dagger-xmts\");\nawait moveFile(\"items\", \"draakhorn\", \"draakhorn-rot\");\nawait moveFile(\"items\", \"draconic-longsword\", \"draconic-longsword-toa\");\nawait moveFile(\"items\", \"dragon-sensing-longsword\", \"dragon-sensing-longsword-pota\");\nawait moveFile(\"items\", \"dragon-thighbone-club\", \"dragon-thighbone-club-skt\");\nawait moveFile(\"items\", \"dragon\", \"dragon-wdh\");\nawait moveFile(\"items\", \"dragongleam\", \"dragongleam-hotdq\");\nawait moveFile(\"items\", \"dragonguard\", \"dragonguard-lmop\");\nawait moveFile(\"items\", \"dragons-blood\", \"dragons-blood-erlw\");\nawait moveFile(\"items\", \"dragonstaff-of-ahghairon\", \"dragonstaff-of-ahghairon-wdh\");\nawait moveFile(\"items\", \"dragontooth-dagger\", \"dragontooth-dagger-rot\");\nawait moveFile(\"items\", \"dread-helm\", \"dread-helm-xge\");\nawait moveFile(\"items\", \"dreamlily\", \"dreamlily-erlw\");\nawait moveFile(\"items\", \"drown\", \"drown-pota\");\nawait moveFile(\"items\", \"duplicitous-manuscript\", \"duplicitous-manuscript-tce\");\nawait moveFile(\"items\", \"duskcrusher\", \"duskcrusher-egw\");\nawait moveFile(\"items\", \"dust-of-corrosion\", \"dust-of-corrosion-wbtw\");\nawait moveFile(\"items\", \"dust-of-deliciousness\", \"dust-of-deliciousness-egw\");\nawait moveFile(\"items\", \"dust-of-the-mummy\", \"dust-of-the-mummy-imr\");\nawait moveFile(\"items\", \"dyrrns-tentacle-whip\", \"dyrrns-tentacle-whip-erlw\");\nawait moveFile(\"items\", \"eagle-whistle\", \"eagle-whistle-tftyp\");\nawait moveFile(\"items\", \"ear-horn-of-hearing\", \"ear-horn-of-hearing-xge\");\nawait moveFile(\"items\", \"earring-of-message\", \"earring-of-message-crcotn\");\nawait moveFile(\"items\", \"earworm\", \"earworm-erlw\");\nawait moveFile(\"items\", \"elder-cartographers-glossography\", \"elder-cartographers-glossography-ai\");\nawait moveFile(\"items\", \"eldritch-claw-tattoo\", \"eldritch-claw-tattoo-ua2020spellsandmagictattoos\");\nawait moveFile(\"items\", \"eldritch-staff\", \"eldritch-staff-wbtw\");\nawait moveFile(\"items\", \"electrum-50-zib-coin\", \"electrum-50-zib-coin-ggr\");\nawait moveFile(\"items\", \"elemental-essence-shard-air\", \"elemental-essence-shard-air-tce\");\nawait moveFile(\"items\", \"elemental-essence-shard-earth\", \"elemental-essence-shard-earth-tce\");\nawait moveFile(\"items\", \"elemental-essence-shard-fire\", \"elemental-essence-shard-fire-tce\");\nawait moveFile(\"items\", \"elemental-essence-shard\", \"elemental-essence-shard-tce\");\nawait moveFile(\"items\", \"elemental-essence-shard-water\", \"elemental-essence-shard-water-tce\");\nawait moveFile(\"items\", \"elven-trinket\", \"elven-trinket-mtf\");\nawait moveFile(\"items\", \"emerald-pen\", \"emerald-pen-ftd\");\nawait moveFile(\"items\", \"enchanted-three-dragon-ante-set\", \"enchanted-three-dragon-ante-set-llk\");\nawait moveFile(\"items\", \"enduring-spellbook\", \"enduring-spellbook-xge\");\nawait moveFile(\"items\", \"ephixis-bow-of-nylea\", \"ephixis-bow-of-nylea-mot\");\nawait moveFile(\"items\", \"ersatz-eye\", \"ersatz-eye-xge\");\nawait moveFile(\"items\", \"everbright-lantern\", \"everbright-lantern-erlw\");\nawait moveFile(\"items\", \"explosive-seed\", \"explosive-seed-egw\");\nawait moveFile(\"items\", \"eye-patch-with-a-mock-eye-set-in-blue-sapphire-and-moonstone-brazen-coalition\", \"eye-patch-with-a-mock-eye-set-in-blue-sapphire-and-moonstone-brazen-coalition-psx\");\nawait moveFile(\"items\", \"faerie-dust\", \"faerie-dust-skt\");\nawait moveFile(\"items\", \"failed-experiment-wand\", \"failed-experiment-wand-ai\");\nawait moveFile(\"items\", \"falkirs-helm-of-pigheadedness\", \"falkirs-helm-of-pigheadedness-wdmm\");\nawait moveFile(\"items\", \"fane-eater\", \"fane-eater-bgdia\");\nawait moveFile(\"items\", \"far-gear\", \"far-gear-ai\");\nawait moveFile(\"items\", \"far-realm-shard\", \"far-realm-shard-tce\");\nawait moveFile(\"items\", \"feather-of-diatryma-summoning\", \"feather-of-diatryma-summoning-wdh\");\nawait moveFile(\"items\", \"feather-token\", \"feather-token-erlw\");\nawait moveFile(\"items\", \"feathered-mantle-with-emerald-clasp-sun-empire\", \"feathered-mantle-with-emerald-clasp-sun-empire-psx\");\nawait moveFile(\"items\", \"feywild-shard\", \"feywild-shard-tce\");\nawait moveFile(\"items\", \"feywild-trinket\", \"feywild-trinket-wbtw\");\nawait moveFile(\"items\", \"finders-goggles\", \"finders-goggles-erlw\");\nawait moveFile(\"items\", \"fine-gold-chain-with-fire-opals-legion-of-dusk\", \"fine-gold-chain-with-fire-opals-legion-of-dusk-psx\");\nawait moveFile(\"items\", \"fine-robe-with-dinosaur-feathers-and-silver-embroidery-sun-empire\", \"fine-robe-with-dinosaur-feathers-and-silver-embroidery-sun-empire-psx\");\nawait moveFile(\"items\", \"fine-steel-rapier-with-gold-filigree-hilt-legion-of-dusk\", \"fine-steel-rapier-with-gold-filigree-hilt-legion-of-dusk-psx\");\nawait moveFile(\"items\", \"finely-articulated-jade-glove-river-heralds\", \"finely-articulated-jade-glove-river-heralds-psx\");\nawait moveFile(\"items\", \"fire-absorbing-tattoo\", \"fire-absorbing-tattoo-tce\");\nawait moveFile(\"items\", \"fish-suit\", \"fish-suit-aag\");\nawait moveFile(\"items\", \"flail-of-tiamat\", \"flail-of-tiamat-ftd\");\nawait moveFile(\"items\", \"flame-tongue-shortsword-of-greed\", \"flame-tongue-shortsword-of-greed-tftyp\");\nawait moveFile(\"items\", \"flensing-claws-huge\", \"flensing-claws-huge-vgm\");\nawait moveFile(\"items\", \"flensing-claws-large\", \"flensing-claws-large-vgm\");\nawait moveFile(\"items\", \"flensing-claws-medium\", \"flensing-claws-medium-vgm\");\nawait moveFile(\"items\", \"flensing-claws-small\", \"flensing-claws-small-vgm\");\nawait moveFile(\"items\", \"flying-chariot\", \"flying-chariot-mot\");\nawait moveFile(\"items\", \"flying-fish-ship\", \"flying-fish-ship-aag\");\nawait moveFile(\"items\", \"force-absorbing-tattoo\", \"force-absorbing-tattoo-tce\");\nawait moveFile(\"items\", \"fulminating-treatise\", \"fulminating-treatise-tce\");\nawait moveFile(\"items\", \"galders-bubble-pipe\", \"galders-bubble-pipe-llk\");\nawait moveFile(\"items\", \"gauntlets-of-flaming-fury\", \"gauntlets-of-flaming-fury-bgdia\");\nawait moveFile(\"items\", \"gavel-of-the-venn-rune\", \"gavel-of-the-venn-rune-skt\");\nawait moveFile(\"items\", \"ghost-lantern\", \"ghost-lantern-toa\");\nawait moveFile(\"items\", \"ghost-step-tattoo\", \"ghost-step-tattoo-ua2020spellsandmagictattoos\");\nawait moveFile(\"items\", \"glaur\", \"glaur-scag\");\nawait moveFile(\"items\", \"glove-puppet-in-the-shape-of-a-wizard\", \"glove-puppet-in-the-shape-of-a-wizard-wbtw\");\nawait moveFile(\"items\", \"gloves-of-soul-catching\", \"gloves-of-soul-catching-cm\");\nawait moveFile(\"items\", \"gnomengarde-grenade\", \"gnomengarde-grenade-dc\");\nawait moveFile(\"items\", \"goggles-of-object-reading\", \"goggles-of-object-reading-egw\");\nawait moveFile(\"items\", \"gold-5-zino-coin\", \"gold-5-zino-coin-ggr\");\nawait moveFile(\"items\", \"gold-basin-with-rubies-legion-of-dusk\", \"gold-basin-with-rubies-legion-of-dusk-psx\");\nawait moveFile(\"items\", \"gold-canary-figurine-of-wondrous-power\", \"gold-canary-figurine-of-wondrous-power-ftd\");\nawait moveFile(\"items\", \"gold-chalice-legion-of-dusk\", \"gold-chalice-legion-of-dusk-psx\");\nawait moveFile(\"items\", \"gold-chalice-set-with-emeralds-legion-of-dusk\", \"gold-chalice-set-with-emeralds-legion-of-dusk-psx\");\nawait moveFile(\"items\", \"gold-jewelry-box-with-platinum-filigree-brazen-coalition\", \"gold-jewelry-box-with-platinum-filigree-brazen-coalition-psx\");\nawait moveFile(\"items\", \"gold-locket-with-a-painted-portrait-inside-legion-of-dusk\", \"gold-locket-with-a-painted-portrait-inside-legion-of-dusk-psx\");\nawait moveFile(\"items\", \"gold-music-box-brazen-coalition\", \"gold-music-box-brazen-coalition-psx\");\nawait moveFile(\"items\", \"gold-pendant-with-black-onyx-legion-of-dusk\", \"gold-pendant-with-black-onyx-legion-of-dusk-psx\");\nawait moveFile(\"items\", \"gold-plated-ceremonial-helmet-and-pauldrons-legion-of-dusk\", \"gold-plated-ceremonial-helmet-and-pauldrons-legion-of-dusk-psx\");\nawait moveFile(\"items\", \"gold-plated-sextant-with-topaz-brazen-coalition\", \"gold-plated-sextant-with-topaz-brazen-coalition-psx\");\nawait moveFile(\"items\", \"gold-ring-with-turquoise-legion-of-dusk\", \"gold-ring-with-turquoise-legion-of-dusk-psx\");\nawait moveFile(\"items\", \"gold-zino\", \"gold-zino-ggr\");\nawait moveFile(\"items\", \"golgari-guild-signet\", \"golgari-guild-signet-ggr\");\nawait moveFile(\"items\", \"golgari-keyrune\", \"golgari-keyrune-ggr\");\nawait moveFile(\"items\", \"gravenhollow-compass-ring\", \"gravenhollow-compass-ring-oota\");\nawait moveFile(\"items\", \"greater-silver-sword\", \"greater-silver-sword-mtf\");\nawait moveFile(\"items\", \"green-chromatic-rose\", \"green-chromatic-rose-wbtw\");\nawait moveFile(\"items\", \"green-copper-ewer\", \"green-copper-ewer-cos\");\nawait moveFile(\"items\", \"green-dragon-mask\", \"green-dragon-mask-rotos\");\nawait moveFile(\"items\", \"grimoire-infinitus-awakened\", \"grimoire-infinitus-awakened-egw\");\nawait moveFile(\"items\", \"grimoire-infinitus-dormant\", \"grimoire-infinitus-dormant-egw\");\nawait moveFile(\"items\", \"grimoire-infinitus-exalted\", \"grimoire-infinitus-exalted-egw\");\nawait moveFile(\"items\", \"grovelthrash-awakened\", \"grovelthrash-awakened-egw\");\nawait moveFile(\"items\", \"grovelthrash-dormant\", \"grovelthrash-dormant-egw\");\nawait moveFile(\"items\", \"grovelthrash-exalted\", \"grovelthrash-exalted-egw\");\nawait moveFile(\"items\", \"gruul-guild-signet\", \"gruul-guild-signet-ggr\");\nawait moveFile(\"items\", \"gruul-keyrune\", \"gruul-keyrune-ggr\");\nawait moveFile(\"items\", \"guardian-emblem\", \"guardian-emblem-tce\");\nawait moveFile(\"items\", \"gulthias-staff\", \"gulthias-staff-cos\");\nawait moveFile(\"items\", \"gurts-greataxe\", \"gurts-greataxe-skt\");\nawait moveFile(\"items\", \"hacking-tools\", \"hacking-tools-uamodernmagic\");\nawait moveFile(\"items\", \"hammerhead-ship\", \"hammerhead-ship-aag\");\nawait moveFile(\"items\", \"hammock-of-worlds\", \"hammock-of-worlds-jttrc\");\nawait moveFile(\"items\", \"hand-drum\", \"hand-drum-scag\");\nawait moveFile(\"items\", \"harbor-moon\", \"harbor-moon-wdh\");\nawait moveFile(\"items\", \"harkons-bite\", \"harkons-bite-vrgr\");\nawait moveFile(\"items\", \"hat-of-vermin\", \"hat-of-vermin-xge\");\nawait moveFile(\"items\", \"hat-of-wizardry\", \"hat-of-wizardry-xge\");\nawait moveFile(\"items\", \"hazirawn\", \"hazirawn-hotdq\");\nawait moveFile(\"items\", \"heart-weavers-primer\", \"heart-weavers-primer-tce\");\nawait moveFile(\"items\", \"hell-hound-cloak\", \"hell-hound-cloak-tftyp\");\nawait moveFile(\"items\", \"helm-of-devil-command\", \"helm-of-devil-command-bgdia\");\nawait moveFile(\"items\", \"helm-of-the-gods\", \"helm-of-the-gods-mot\");\nawait moveFile(\"items\", \"helm-of-the-scavenger\", \"helm-of-the-scavenger-wdmm\");\nawait moveFile(\"items\", \"helm-of-underwater-action\", \"helm-of-underwater-action-gos\");\nawait moveFile(\"items\", \"hew\", \"hew-lmop\");\nawait moveFile(\"items\", \"hewards-handy-spice-pouch\", \"hewards-handy-spice-pouch-xge\");\nawait moveFile(\"items\", \"hewards-hireling-armor\", \"hewards-hireling-armor-llk\");\nawait moveFile(\"items\", \"hide-of-the-feral-guardian-awakened\", \"hide-of-the-feral-guardian-awakened-egw\");\nawait moveFile(\"items\", \"hide-of-the-feral-guardian-dormant\", \"hide-of-the-feral-guardian-dormant-egw\");\nawait moveFile(\"items\", \"hide-of-the-feral-guardian-exalted\", \"hide-of-the-feral-guardian-exalted-egw\");\nawait moveFile(\"items\", \"holy-symbol-of-ravenkind\", \"holy-symbol-of-ravenkind-cos\");\nawait moveFile(\"items\", \"hook-of-fishers-delight\", \"hook-of-fishers-delight-idrotf\");\nawait moveFile(\"items\", \"hooked-shortspear\", \"hooked-shortspear-oota\");\nawait moveFile(\"items\", \"horn-of-silent-alarm\", \"horn-of-silent-alarm-xge\");\nawait moveFile(\"items\", \"horn-of-the-endless-maze\", \"horn-of-the-endless-maze-wdmm\");\nawait moveFile(\"items\", \"horned-ring\", \"horned-ring-wdmm\");\nawait moveFile(\"items\", \"horror-trinket\", \"horror-trinket-vrgr\");\nawait moveFile(\"items\", \"hunters-coat\", \"hunters-coat-egw\");\nawait moveFile(\"items\", \"icewind-dale-trinket\", \"icewind-dale-trinket-idrotf\");\nawait moveFile(\"items\", \"icon-of-ravenloft\", \"icon-of-ravenloft-cos\");\nawait moveFile(\"items\", \"iggwilvs-cauldron\", \"iggwilvs-cauldron-wbtw\");\nawait moveFile(\"items\", \"illuminators-tattoo\", \"illuminators-tattoo-tce\");\nawait moveFile(\"items\", \"illusionists-bracers\", \"illusionists-bracers-ggr\");\nawait moveFile(\"items\", \"infernal-puzzle-box\", \"infernal-puzzle-box-bgdia\");\nawait moveFile(\"items\", \"infernal-tack\", \"infernal-tack-mtf\");\nawait moveFile(\"items\", \"infiltrators-key-awakened\", \"infiltrators-key-awakened-egw\");\nawait moveFile(\"items\", \"infiltrators-key-dormant\", \"infiltrators-key-dormant-egw\");\nawait moveFile(\"items\", \"infiltrators-key-exalted\", \"infiltrators-key-exalted-egw\");\nawait moveFile(\"items\", \"ingot-of-the-skold-rune\", \"ingot-of-the-skold-rune-skt\");\nawait moveFile(\"items\", \"insect-repellent-block-of-incense\", \"insect-repellent-block-of-incense-toa\");\nawait moveFile(\"items\", \"insect-repellent-greasy-salve\", \"insect-repellent-greasy-salve-toa\");\nawait moveFile(\"items\", \"insignia-of-claws\", \"insignia-of-claws-hotdq\");\nawait moveFile(\"items\", \"instrument-of-illusions\", \"instrument-of-illusions-xge\");\nawait moveFile(\"items\", \"instrument-of-scribing\", \"instrument-of-scribing-xge\");\nawait moveFile(\"items\", \"ioun-stone-historical-knowledge\", \"ioun-stone-historical-knowledge-llk\");\nawait moveFile(\"items\", \"ioun-stone-language-knowledge\", \"ioun-stone-language-knowledge-llk\");\nawait moveFile(\"items\", \"ioun-stone-natural-knowledge\", \"ioun-stone-natural-knowledge-llk\");\nawait moveFile(\"items\", \"ioun-stone-of-vitality\", \"ioun-stone-of-vitality-imr\");\nawait moveFile(\"items\", \"ioun-stone-religious-knowledge\", \"ioun-stone-religious-knowledge-llk\");\nawait moveFile(\"items\", \"ioun-stone-self-preservation\", \"ioun-stone-self-preservation-llk\");\nawait moveFile(\"items\", \"ioun-stone-supreme-intellect\", \"ioun-stone-supreme-intellect-llk\");\nawait moveFile(\"items\", \"iron-ball\", \"iron-ball-idrotf\");\nawait moveFile(\"items\", \"ironfang\", \"ironfang-pota\");\nawait moveFile(\"items\", \"ivanas-whisper\", \"ivanas-whisper-vrgr\");\nawait moveFile(\"items\", \"izzet-guild-signet\", \"izzet-guild-signet-ggr\");\nawait moveFile(\"items\", \"izzet-keyrune\", \"izzet-keyrune-ggr\");\nawait moveFile(\"items\", \"jade-bowl-river-heralds\", \"jade-bowl-river-heralds-psx\");\nawait moveFile(\"items\", \"jade-breastplate-river-heralds\", \"jade-breastplate-river-heralds-psx\");\nawait moveFile(\"items\", \"jade-headpiece-river-heralds\", \"jade-headpiece-river-heralds-psx\");\nawait moveFile(\"items\", \"jade-serpent-staff\", \"jade-serpent-staff-wdmm\");\nawait moveFile(\"items\", \"jade-sword-with-amber-river-heralds\", \"jade-sword-with-amber-river-heralds-psx\");\nawait moveFile(\"items\", \"jade-totem-with-diamond-eyes-river-heralds\", \"jade-totem-with-diamond-eyes-river-heralds-psx\");\nawait moveFile(\"items\", \"jakarions-staff-of-frost\", \"jakarions-staff-of-frost-cos\");\nawait moveFile(\"items\", \"javelin-of-backbiting\", \"javelin-of-backbiting-tftyp\");\nawait moveFile(\"items\", \"jewel-of-three-prayers-awakened\", \"jewel-of-three-prayers-awakened-crcotn\");\nawait moveFile(\"items\", \"jewel-of-three-prayers-dormant\", \"jewel-of-three-prayers-dormant-crcotn\");\nawait moveFile(\"items\", \"jewel-of-three-prayers-exalted\", \"jewel-of-three-prayers-exalted-crcotn\");\nawait moveFile(\"items\", \"jeweled-anklet-brazen-coalition\", \"jeweled-anklet-brazen-coalition-psx\");\nawait moveFile(\"items\", \"junky-1-dagger\", \"junky-1-dagger-tftyp\");\nawait moveFile(\"items\", \"keg-of-alchemists-fire\", \"keg-of-alchemists-fire-aag\");\nawait moveFile(\"items\", \"keycharm\", \"keycharm-erlw\");\nawait moveFile(\"items\", \"keystone-of-creation\", \"keystone-of-creation-aitfr-avt\");\nawait moveFile(\"items\", \"khrusor-spear-of-heliod\", \"khrusor-spear-of-heliod-mot\");\nawait moveFile(\"items\", \"knaves-eye-patch\", \"knaves-eye-patch-wdh\");\nawait moveFile(\"items\", \"korolnor-scepter\", \"korolnor-scepter-skt\");\nawait moveFile(\"items\", \"kyrzins-ooze\", \"kyrzins-ooze-erlw\");\nawait moveFile(\"items\", \"lamprey-ship\", \"lamprey-ship-aag\");\nawait moveFile(\"items\", \"lantern-of-tracking\", \"lantern-of-tracking-idrotf\");\nawait moveFile(\"items\", \"large-gold-bracelet-legion-of-dusk\", \"large-gold-bracelet-legion-of-dusk-psx\");\nawait moveFile(\"items\", \"large-jade-totem-river-heralds\", \"large-jade-totem-river-heralds-psx\");\nawait moveFile(\"items\", \"large-well-made-tapestry-legion-of-dusk\", \"large-well-made-tapestry-legion-of-dusk-psx\");\nawait moveFile(\"items\", \"lash-of-shadows-awakened\", \"lash-of-shadows-awakened-egw\");\nawait moveFile(\"items\", \"lash-of-shadows-dormant\", \"lash-of-shadows-dormant-egw\");\nawait moveFile(\"items\", \"lash-of-shadows-exalted\", \"lash-of-shadows-exalted-egw\");\nawait moveFile(\"items\", \"leather-golem-armor\", \"leather-golem-armor-llk\");\nawait moveFile(\"items\", \"lesser-hammock-of-worlds\", \"lesser-hammock-of-worlds-jttrc\");\nawait moveFile(\"items\", \"libram-of-souls-and-flesh\", \"libram-of-souls-and-flesh-tce\");\nawait moveFile(\"items\", \"lifewell-tattoo\", \"lifewell-tattoo-tce\");\nawait moveFile(\"items\", \"light-repeating-crossbow\", \"light-repeating-crossbow-oota\");\nawait moveFile(\"items\", \"lightbringer\", \"lightbringer-lmop\");\nawait moveFile(\"items\", \"lightning-absorbing-tattoo\", \"lightning-absorbing-tattoo-tce\");\nawait moveFile(\"items\", \"living-gloves\", \"living-gloves-erlw\");\nawait moveFile(\"items\", \"living-loot-satchel\", \"living-loot-satchel-ai\");\nawait moveFile(\"items\", \"living-ship\", \"living-ship-aag\");\nawait moveFile(\"items\", \"loadstone\", \"loadstone-tftyp\");\nawait moveFile(\"items\", \"lock-of-trickery\", \"lock-of-trickery-xge\");\nawait moveFile(\"items\", \"longhorn\", \"longhorn-scag\");\nawait moveFile(\"items\", \"lords-ensemble\", \"lords-ensemble-wdh\");\nawait moveFile(\"items\", \"lorehold-primer\", \"lorehold-primer-scc\");\nawait moveFile(\"items\", \"lost-crown-of-besilmer\", \"lost-crown-of-besilmer-pota\");\nawait moveFile(\"items\", \"lost-sword\", \"lost-sword-cos\");\nawait moveFile(\"items\", \"lubas-tarokka-of-souls\", \"lubas-tarokka-of-souls-tce\");\nawait moveFile(\"items\", \"lute-crafted-of-exotic-wood-with-mother-of-pearl-inlay-and-zircon-gems-brazen-coalition\", \"lute-crafted-of-exotic-wood-with-mother-of-pearl-inlay-and-zircon-gems-brazen-coalition-psx\");\nawait moveFile(\"items\", \"luxon-beacon\", \"luxon-beacon-egw\");\nawait moveFile(\"items\", \"lyre-of-building\", \"lyre-of-building-tce\");\nawait moveFile(\"items\", \"mace-of-the-black-crown-awakened\", \"mace-of-the-black-crown-awakened-egw\");\nawait moveFile(\"items\", \"mace-of-the-black-crown-dormant\", \"mace-of-the-black-crown-dormant-egw\");\nawait moveFile(\"items\", \"mace-of-the-black-crown-exalted\", \"mace-of-the-black-crown-exalted-egw\");\nawait moveFile(\"items\", \"macuahuitl\", \"macuahuitl-tftyp\");\nawait moveFile(\"items\", \"maddgoths-helm\", \"maddgoths-helm-wdmm\");\nawait moveFile(\"items\", \"marble-font-with-gold-inlay-legion-of-dusk\", \"marble-font-with-gold-inlay-legion-of-dusk-psx\");\nawait moveFile(\"items\", \"mask-of-the-beast\", \"mask-of-the-beast-toa\");\nawait moveFile(\"items\", \"mask-of-the-dragon-queen\", \"mask-of-the-dragon-queen-rot\");\nawait moveFile(\"items\", \"masque-charm\", \"masque-charm-scc\");\nawait moveFile(\"items\", \"masquerade-tattoo\", \"masquerade-tattoo-ua2020spellsandmagictattoos\");\nawait moveFile(\"items\", \"masterpiece-painting-in-mahogany-frame-with-gold-inlay-legion-of-dusk\", \"masterpiece-painting-in-mahogany-frame-with-gold-inlay-legion-of-dusk-psx\");\nawait moveFile(\"items\", \"mastix-whip-of-erebos\", \"mastix-whip-of-erebos-mot\");\nawait moveFile(\"items\", \"matalotok\", \"matalotok-bgdia\");\nawait moveFile(\"items\", \"matchless-pipe\", \"matchless-pipe-wdh\");\nawait moveFile(\"items\", \"medal-of-muscle\", \"medal-of-muscle-crcotn\");\nawait moveFile(\"items\", \"medal-of-the-conch\", \"medal-of-the-conch-crcotn\");\nawait moveFile(\"items\", \"medal-of-the-horizonback\", \"medal-of-the-horizonback-crcotn\");\nawait moveFile(\"items\", \"medal-of-the-maze\", \"medal-of-the-maze-crcotn\");\nawait moveFile(\"items\", \"medal-of-the-meat-pie\", \"medal-of-the-meat-pie-crcotn\");\nawait moveFile(\"items\", \"medal-of-the-wetlands\", \"medal-of-the-wetlands-crcotn\");\nawait moveFile(\"items\", \"medal-of-wit\", \"medal-of-wit-crcotn\");\nawait moveFile(\"items\", \"menga-leaves-1-ounce\", \"menga-leaves-1-ounce-toa\");\nawait moveFile(\"items\", \"mighty-servant-of-leuk-o\", \"mighty-servant-of-leuk-o-tce\");\nawait moveFile(\"items\", \"mind-flayer-skull\", \"mind-flayer-skull-wdmm\");\nawait moveFile(\"items\", \"mind-lash\", \"mind-lash-vgm\");\nawait moveFile(\"items\", \"mirror-of-the-past\", \"mirror-of-the-past-tftyp\");\nawait moveFile(\"items\", \"mizzium-apparatus\", \"mizzium-apparatus-ggr\");\nawait moveFile(\"items\", \"mizzium-mortar\", \"mizzium-mortar-ggr\");\nawait moveFile(\"items\", \"monster-hunters-pack\", \"monster-hunters-pack-vrgr\");\nawait moveFile(\"items\", \"moodmark-paint\", \"moodmark-paint-ggr\");\nawait moveFile(\"items\", \"moorbounder\", \"moorbounder-egw\");\nawait moveFile(\"items\", \"mummy-rot-antidote\", \"mummy-rot-antidote-imr\");\nawait moveFile(\"items\", \"murgaxors-elixir-of-life\", \"murgaxors-elixir-of-life-scc\");\nawait moveFile(\"items\", \"murgaxors-orb\", \"murgaxors-orb-scc\");\nawait moveFile(\"items\", \"muroosa-balm\", \"muroosa-balm-egw\");\nawait moveFile(\"items\", \"mystery-key\", \"mystery-key-xge\");\nawait moveFile(\"items\", \"natures-mantle\", \"natures-mantle-tce\");\nawait moveFile(\"items\", \"nautiloid\", \"nautiloid-aag\");\nawait moveFile(\"items\", \"navigation-orb\", \"navigation-orb-skt\");\nawait moveFile(\"items\", \"necklace-of-electrum-medallions-with-red-and-blue-tournalines-brazen-coalition\", \"necklace-of-electrum-medallions-with-red-and-blue-tournalines-brazen-coalition-psx\");\nawait moveFile(\"items\", \"necklace-of-jade-and-pink-pearls-river-heralds\", \"necklace-of-jade-and-pink-pearls-river-heralds-psx\");\nawait moveFile(\"items\", \"necrotic-absorbing-tattoo\", \"necrotic-absorbing-tattoo-tce\");\nawait moveFile(\"items\", \"needle-of-mending\", \"needle-of-mending-egw\");\nawait moveFile(\"items\", \"nepenthe\", \"nepenthe-vrgr\");\nawait moveFile(\"items\", \"nether-scroll-of-azumar\", \"nether-scroll-of-azumar-cm\");\nawait moveFile(\"items\", \"nib\", \"nib-wdh\");\nawait moveFile(\"items\", \"night-caller\", \"night-caller-tftyp\");\nawait moveFile(\"items\", \"nightfall-pearl\", \"nightfall-pearl-egw\");\nawait moveFile(\"items\", \"nightspider\", \"nightspider-aag\");\nawait moveFile(\"items\", \"nimblewright-detector\", \"nimblewright-detector-wdh\");\nawait moveFile(\"items\", \"obsidian-flint-dragon-plate\", \"obsidian-flint-dragon-plate-bgdia\");\nawait moveFile(\"items\", \"obviators-lenses\", \"obviators-lenses-ai\");\nawait moveFile(\"items\", \"occultant-abacus\", \"occultant-abacus-ai\");\nawait moveFile(\"items\", \"olisuba-leaf\", \"olisuba-leaf-egw\");\nawait moveFile(\"items\", \"opal-of-the-ild-rune\", \"opal-of-the-ild-rune-skt\");\nawait moveFile(\"items\", \"orb-of-direction\", \"orb-of-direction-xge\");\nawait moveFile(\"items\", \"orb-of-gonging\", \"orb-of-gonging-wdmm\");\nawait moveFile(\"items\", \"orb-of-the-stein-rune\", \"orb-of-the-stein-rune-uaprestigeclassesrunmagic\");\nawait moveFile(\"items\", \"orb-of-the-veil\", \"orb-of-the-veil-egw\");\nawait moveFile(\"items\", \"orb-of-time\", \"orb-of-time-xge\");\nawait moveFile(\"items\", \"orc-stone\", \"orc-stone-idrotf\");\nawait moveFile(\"items\", \"orcsplitter\", \"orcsplitter-pota\");\nawait moveFile(\"items\", \"orcus-figurine\", \"orcus-figurine-cm\");\nawait moveFile(\"items\", \"order-of-the-silver-dragon-2-shield\", \"order-of-the-silver-dragon-2-shield-cos\");\nawait moveFile(\"items\", \"ornithopter-of-flying\", \"ornithopter-of-flying-wbtw\");\nawait moveFile(\"items\", \"orrery-of-the-wanderer\", \"orrery-of-the-wanderer-ai\");\nawait moveFile(\"items\", \"orzhov-guild-signet\", \"orzhov-guild-signet-ggr\");\nawait moveFile(\"items\", \"orzhov-keyrune\", \"orzhov-keyrune-ggr\");\nawait moveFile(\"items\", \"outer-essence-shard-chaotic\", \"outer-essence-shard-chaotic-tce\");\nawait moveFile(\"items\", \"outer-essence-shard-evil\", \"outer-essence-shard-evil-tce\");\nawait moveFile(\"items\", \"outer-essence-shard-good\", \"outer-essence-shard-good-tce\");\nawait moveFile(\"items\", \"outer-essence-shard-lawful\", \"outer-essence-shard-lawful-tce\");\nawait moveFile(\"items\", \"outer-essence-shard\", \"outer-essence-shard-tce\");\nawait moveFile(\"items\", \"oversized-longbow\", \"oversized-longbow-wdh\");\nawait moveFile(\"items\", \"pair-of-engraved-bone-dice-brazen-coalition\", \"pair-of-engraved-bone-dice-brazen-coalition-psx\");\nawait moveFile(\"items\", \"paper-bird\", \"paper-bird-wdh\");\nawait moveFile(\"items\", \"pariahs-shield\", \"pariahs-shield-ggr\");\nawait moveFile(\"items\", \"pathfinders-greataxe\", \"pathfinders-greataxe-pota\");\nawait moveFile(\"items\", \"pearl-of-undead-detection\", \"pearl-of-undead-detection-wdmm\");\nawait moveFile(\"items\", \"pennant-of-the-vind-rune\", \"pennant-of-the-vind-rune-uaprestigeclassesrunmagic\");\nawait moveFile(\"items\", \"peregrine-mask\", \"peregrine-mask-ggr\");\nawait moveFile(\"items\", \"perfume-of-bewitching\", \"perfume-of-bewitching-xge\");\nawait moveFile(\"items\", \"petrified-grung-egg\", \"petrified-grung-egg-toa\");\nawait moveFile(\"items\", \"pewter-mug-with-green-spinels-brazen-coalition\", \"pewter-mug-with-green-spinels-brazen-coalition-psx\");\nawait moveFile(\"items\", \"piercer\", \"piercer-ai\");\nawait moveFile(\"items\", \"pipe-of-remembrance\", \"pipe-of-remembrance-gos\");\nawait moveFile(\"items\", \"pipe-of-smoke-monsters\", \"pipe-of-smoke-monsters-xge\");\nawait moveFile(\"items\", \"pirates-cutlass\", \"pirates-cutlass-xmts\");\nawait moveFile(\"items\", \"piwafwi-cloak-of-elvenkind\", \"piwafwi-cloak-of-elvenkind-oota\");\nawait moveFile(\"items\", \"piwafwi-of-fire-resistance\", \"piwafwi-of-fire-resistance-oota\");\nawait moveFile(\"items\", \"pixie-dust\", \"pixie-dust-wbtw\");\nawait moveFile(\"items\", \"planar-key\", \"planar-key-aitfr-thp\");\nawait moveFile(\"items\", \"planecallers-codex\", \"planecallers-codex-tce\");\nawait moveFile(\"items\", \"platinum-10-zino-coin\", \"platinum-10-zino-coin-ggr\");\nawait moveFile(\"items\", \"platinum-100-zino-coin\", \"platinum-100-zino-coin-ggr\");\nawait moveFile(\"items\", \"platinum-headdress-with-topaz-sun-symbol-sun-empire\", \"platinum-headdress-with-topaz-sun-symbol-sun-empire-psx\");\nawait moveFile(\"items\", \"platinum-ring-with-yellow-sapphire-sun-empire\", \"platinum-ring-with-yellow-sapphire-sun-empire-psx\");\nawait moveFile(\"items\", \"platinum-scarf\", \"platinum-scarf-ftd\");\nawait moveFile(\"items\", \"platinum-staff-topped-with-amber-sun-empire\", \"platinum-staff-topped-with-amber-sun-empire-psx\");\nawait moveFile(\"items\", \"poison-absorbing-tattoo\", \"poison-absorbing-tattoo-tce\");\nawait moveFile(\"items\", \"pole-of-angling\", \"pole-of-angling-xge\");\nawait moveFile(\"items\", \"pole-of-collapsing\", \"pole-of-collapsing-xge\");\nawait moveFile(\"items\", \"portfolio-keeper\", \"portfolio-keeper-ai\");\nawait moveFile(\"items\", \"pot-of-awakening\", \"pot-of-awakening-xge\");\nawait moveFile(\"items\", \"potion-of-advantage\", \"potion-of-advantage-wbtw\");\nawait moveFile(\"items\", \"potion-of-aqueous-form\", \"potion-of-aqueous-form-mot\");\nawait moveFile(\"items\", \"potion-of-comprehension\", \"potion-of-comprehension-wdmm\");\nawait moveFile(\"items\", \"potion-of-dragons-majesty\", \"potion-of-dragons-majesty-ftd\");\nawait moveFile(\"items\", \"potion-of-giant-size\", \"potion-of-giant-size-skt\");\nawait moveFile(\"items\", \"potion-of-maximum-power\", \"potion-of-maximum-power-egw\");\nawait moveFile(\"items\", \"potion-of-mind-control-beast\", \"potion-of-mind-control-beast-tftyp\");\nawait moveFile(\"items\", \"potion-of-mind-control-humanoid\", \"potion-of-mind-control-humanoid-tftyp\");\nawait moveFile(\"items\", \"potion-of-mind-control-monster\", \"potion-of-mind-control-monster-tftyp\");\nawait moveFile(\"items\", \"potion-of-possibility\", \"potion-of-possibility-egw\");\nawait moveFile(\"items\", \"potion-of-watchful-rest\", \"potion-of-watchful-rest-wdmm\");\nawait moveFile(\"items\", \"powered-armor\", \"powered-armor-llk\");\nawait moveFile(\"items\", \"pressure-capsule\", \"pressure-capsule-gos\");\nawait moveFile(\"items\", \"pride-silk\", \"pride-silk-egw\");\nawait moveFile(\"items\", \"pride-silk-outfit\", \"pride-silk-outfit-egw\");\nawait moveFile(\"items\", \"primal-amulet\", \"primal-amulet-xmts\");\nawait moveFile(\"items\", \"prismari-primer\", \"prismari-primer-scc\");\nawait moveFile(\"items\", \"prismatic-well\", \"prismatic-well-nrh-at\");\nawait moveFile(\"items\", \"professor-orb\", \"professor-orb-wdmm\");\nawait moveFile(\"items\", \"professor-skant\", \"professor-skant-idrotf\");\nawait moveFile(\"items\", \"propeller-helm\", \"propeller-helm-wdmm\");\nawait moveFile(\"items\", \"prosthetic-limb\", \"prosthetic-limb-tce\");\nawait moveFile(\"items\", \"protective-verses\", \"protective-verses-tce\");\nawait moveFile(\"items\", \"prying-blade\", \"prying-blade-xmts\");\nawait moveFile(\"items\", \"psi-crystal\", \"psi-crystal-idrotf\");\nawait moveFile(\"items\", \"psychic-absorbing-tattoo\", \"psychic-absorbing-tattoo-tce\");\nawait moveFile(\"items\", \"pyroconverger\", \"pyroconverger-ggr\");\nawait moveFile(\"items\", \"pyxis-of-pandemonium\", \"pyxis-of-pandemonium-mot\");\nawait moveFile(\"items\", \"quandrix-primer\", \"quandrix-primer-scc\");\nawait moveFile(\"items\", \"radiance\", \"radiance-cm\");\nawait moveFile(\"items\", \"radiant-absorbing-tattoo\", \"radiant-absorbing-tattoo-tce\");\nawait moveFile(\"items\", \"rain-catcher\", \"rain-catcher-toa\");\nawait moveFile(\"items\", \"rakdos-guild-signet\", \"rakdos-guild-signet-ggr\");\nawait moveFile(\"items\", \"rakdos-keyrune\", \"rakdos-keyrune-ggr\");\nawait moveFile(\"items\", \"rakdos-riteknife\", \"rakdos-riteknife-ggr\");\nawait moveFile(\"items\", \"red-chromatic-rose\", \"red-chromatic-rose-wbtw\");\nawait moveFile(\"items\", \"red-dragon-mask\", \"red-dragon-mask-rotos\");\nawait moveFile(\"items\", \"reincarnation-dust\", \"reincarnation-dust-egw\");\nawait moveFile(\"items\", \"reszur\", \"reszur-pota\");\nawait moveFile(\"items\", \"revelers-concertina\", \"revelers-concertina-tce\");\nawait moveFile(\"items\", \"ring-of-obscuring\", \"ring-of-obscuring-egw\");\nawait moveFile(\"items\", \"ring-of-red-fury\", \"ring-of-red-fury-crcotn\");\nawait moveFile(\"items\", \"ring-of-temporal-salvation\", \"ring-of-temporal-salvation-egw\");\nawait moveFile(\"items\", \"ring-of-truth-telling\", \"ring-of-truth-telling-wdh\");\nawait moveFile(\"items\", \"ring-of-winter\", \"ring-of-winter-toa\");\nawait moveFile(\"items\", \"rings-of-shared-suffering\", \"rings-of-shared-suffering-uawge\");\nawait moveFile(\"items\", \"robe-of-serpents\", \"robe-of-serpents-skt\");\nawait moveFile(\"items\", \"robe-of-summer\", \"robe-of-summer-tftyp\");\nawait moveFile(\"items\", \"rod-of-retribution\", \"rod-of-retribution-egw\");\nawait moveFile(\"items\", \"rod-of-the-vonindod\", \"rod-of-the-vonindod-skt\");\nawait moveFile(\"items\", \"rope-of-mending\", \"rope-of-mending-xge\");\nawait moveFile(\"items\", \"rotor-of-return\", \"rotor-of-return-ai\");\nawait moveFile(\"items\", \"ruby-of-the-war-mage\", \"ruby-of-the-war-mage-xge\");\nawait moveFile(\"items\", \"ruby-weave-gem\", \"ruby-weave-gem-ftd\");\nawait moveFile(\"items\", \"ruidium-shield\", \"ruidium-shield-crcotn\");\nawait moveFile(\"items\", \"ruinblade\", \"ruinblade-imr\");\nawait moveFile(\"items\", \"ruins-wake-awakened\", \"ruins-wake-awakened-egw\");\nawait moveFile(\"items\", \"ruins-wake-dormant\", \"ruins-wake-dormant-egw\");\nawait moveFile(\"items\", \"ruins-wake-exalted\", \"ruins-wake-exalted-egw\");\nawait moveFile(\"items\", \"ruinstone\", \"ruinstone-dc\");\nawait moveFile(\"items\", \"ryath-root\", \"ryath-root-toa\");\nawait moveFile(\"items\", \"saint-markovias-thighbone\", \"saint-markovias-thighbone-cos\");\nawait moveFile(\"items\", \"sapphire-buckler\", \"sapphire-buckler-ftd\");\nawait moveFile(\"items\", \"scatterleaf-tea\", \"scatterleaf-tea-wbtw\");\nawait moveFile(\"items\", \"scissors-of-shadow-snipping\", \"scissors-of-shadow-snipping-wbtw\");\nawait moveFile(\"items\", \"scorpion-armor\", \"scorpion-armor-toa\");\nawait moveFile(\"items\", \"scorpion-ship\", \"scorpion-ship-aag\");\nawait moveFile(\"items\", \"scribes-pen\", \"scribes-pen-erlw\");\nawait moveFile(\"items\", \"scroll-of-tarrasque-summoning\", \"scroll-of-tarrasque-summoning-idrotf\");\nawait moveFile(\"items\", \"scroll-of-the-comet\", \"scroll-of-the-comet-idrotf\");\nawait moveFile(\"items\", \"seeker-dart\", \"seeker-dart-pota\");\nawait moveFile(\"items\", \"sekolahian-worshiping-statuette\", \"sekolahian-worshiping-statuette-gos\");\nawait moveFile(\"items\", \"selesnya-guild-signet\", \"selesnya-guild-signet-ggr\");\nawait moveFile(\"items\", \"selesnya-keyrune\", \"selesnya-keyrune-ggr\");\nawait moveFile(\"items\", \"sending-stone\", \"sending-stone-ai\");\nawait moveFile(\"items\", \"serpent-scale-armor\", \"serpent-scale-armor-cm\");\nawait moveFile(\"items\", \"serpents-fang\", \"serpents-fang-cm\");\nawait moveFile(\"items\", \"shadowfell-brand-tattoo\", \"shadowfell-brand-tattoo-ua2020spellsandmagictattoos\");\nawait moveFile(\"items\", \"shadowfell-shard\", \"shadowfell-shard-tce\");\nawait moveFile(\"items\", \"shard-of-the-ise-rune\", \"shard-of-the-ise-rune-skt\");\nawait moveFile(\"items\", \"shard-of-the-kalt-rune\", \"shard-of-the-kalt-rune-uaprestigeclassesrunmagic\");\nawait moveFile(\"items\", \"shard\", \"shard-wdh\");\nawait moveFile(\"items\", \"shatterspike\", \"shatterspike-tftyp\");\nawait moveFile(\"items\", \"shatterstick\", \"shatterstick-bgdia\");\nawait moveFile(\"items\", \"shield-guardian-amulet\", \"shield-guardian-amulet-idrotf\");\nawait moveFile(\"items\", \"shield-of-expression\", \"shield-of-expression-xge\");\nawait moveFile(\"items\", \"shield-of-far-sight\", \"shield-of-far-sight-vgm\");\nawait moveFile(\"items\", \"shield-of-the-hidden-lord\", \"shield-of-the-hidden-lord-bgdia\");\nawait moveFile(\"items\", \"shield-of-the-uven-rune\", \"shield-of-the-uven-rune-wdmm\");\nawait moveFile(\"items\", \"shiftweave\", \"shiftweave-erlw\");\nawait moveFile(\"items\", \"shrike-ship\", \"shrike-ship-aag\");\nawait moveFile(\"items\", \"silken-spite-awakened\", \"silken-spite-awakened-egw\");\nawait moveFile(\"items\", \"silken-spite-dormant\", \"silken-spite-dormant-egw\");\nawait moveFile(\"items\", \"silken-spite-exalted\", \"silken-spite-exalted-egw\");\nawait moveFile(\"items\", \"silver-25-zib-coin\", \"silver-25-zib-coin-ggr\");\nawait moveFile(\"items\", \"silver-headdress-with-amber-and-red-coral-feathers-sun-empire\", \"silver-headdress-with-amber-and-red-coral-feathers-sun-empire-psx\");\nawait moveFile(\"items\", \"silver-medallion-sun-empire\", \"silver-medallion-sun-empire-psx\");\nawait moveFile(\"items\", \"silver-necklace-with-an-amber-pendant-sun-empire\", \"silver-necklace-with-an-amber-pendant-sun-empire-psx\");\nawait moveFile(\"items\", \"silver-shoulder-piece-with-amber-and-garnet-sun-empire\", \"silver-shoulder-piece-with-amber-and-garnet-sun-empire-psx\");\nawait moveFile(\"items\", \"silver-sword\", \"silver-sword-mtf\");\nawait moveFile(\"items\", \"silverquill-primer\", \"silverquill-primer-scc\");\nawait moveFile(\"items\", \"simic-guild-signet\", \"simic-guild-signet-ggr\");\nawait moveFile(\"items\", \"simic-keyrune\", \"simic-keyrune-ggr\");\nawait moveFile(\"items\", \"sinda-berries-10\", \"sinda-berries-10-toa\");\nawait moveFile(\"items\", \"siren-song-lyre\", \"siren-song-lyre-mot\");\nawait moveFile(\"items\", \"skyblinder-staff\", \"skyblinder-staff-ggr\");\nawait moveFile(\"items\", \"skyship\", \"skyship-egw\");\nawait moveFile(\"items\", \"sled-dog\", \"sled-dog-idrotf\");\nawait moveFile(\"items\", \"sling-bullets-of-althemone\", \"sling-bullets-of-althemone-mot\");\nawait moveFile(\"items\", \"slumbering-dragon-touched-focus\", \"slumbering-dragon-touched-focus-ftd\");\nawait moveFile(\"items\", \"slumbering-dragon-vessel\", \"slumbering-dragon-vessel-ftd\");\nawait moveFile(\"items\", \"slumbering-scaled-ornament\", \"slumbering-scaled-ornament-ftd\");\nawait moveFile(\"items\", \"smokepowder\", \"smokepowder-wdh\");\nawait moveFile(\"items\", \"snicker-snack\", \"snicker-snack-wbtw\");\nawait moveFile(\"items\", \"snowshoes\", \"snowshoes-idrotf\");\nawait moveFile(\"items\", \"songhorn\", \"songhorn-scag\");\nawait moveFile(\"items\", \"soothsalts\", \"soothsalts-egw\");\nawait moveFile(\"items\", \"sorcerous-spyglass\", \"sorcerous-spyglass-xmts\");\nawait moveFile(\"items\", \"soul-coin\", \"soul-coin-bgdia\");\nawait moveFile(\"items\", \"space-galleon\", \"space-galleon-aag\");\nawait moveFile(\"items\", \"speaking-stone\", \"speaking-stone-erlw\");\nawait moveFile(\"items\", \"spear-of-backbiting\", \"spear-of-backbiting-tftyp\");\nawait moveFile(\"items\", \"spell-bottle\", \"spell-bottle-egw\");\nawait moveFile(\"items\", \"spell-gem-amber\", \"spell-gem-amber-oota\");\nawait moveFile(\"items\", \"spell-gem-bloodstone\", \"spell-gem-bloodstone-oota\");\nawait moveFile(\"items\", \"spell-gem-diamond\", \"spell-gem-diamond-oota\");\nawait moveFile(\"items\", \"spell-gem-jade\", \"spell-gem-jade-oota\");\nawait moveFile(\"items\", \"spell-gem-lapis-lazuli\", \"spell-gem-lapis-lazuli-oota\");\nawait moveFile(\"items\", \"spell-gem-obsidian\", \"spell-gem-obsidian-oota\");\nawait moveFile(\"items\", \"spell-gem-quartz\", \"spell-gem-quartz-oota\");\nawait moveFile(\"items\", \"spell-gem-ruby\", \"spell-gem-ruby-oota\");\nawait moveFile(\"items\", \"spell-gem-star-ruby\", \"spell-gem-star-ruby-oota\");\nawait moveFile(\"items\", \"spell-gem-topaz\", \"spell-gem-topaz-oota\");\nawait moveFile(\"items\", \"spelljamming-helm\", \"spelljamming-helm-aag\");\nawait moveFile(\"items\", \"spellshard\", \"spellshard-erlw\");\nawait moveFile(\"items\", \"spellwrought-tattoo-1st-level\", \"spellwrought-tattoo-1st-level-ua2020spellsandmagictattoos\");\nawait moveFile(\"items\", \"spellwrought-tattoo-2nd-level\", \"spellwrought-tattoo-2nd-level-ua2020spellsandmagictattoos\");\nawait moveFile(\"items\", \"spellwrought-tattoo-3rd-level\", \"spellwrought-tattoo-3rd-level-ua2020spellsandmagictattoos\");\nawait moveFile(\"items\", \"spellwrought-tattoo-4th-level\", \"spellwrought-tattoo-4th-level-tce\");\nawait moveFile(\"items\", \"spellwrought-tattoo-5th-level\", \"spellwrought-tattoo-5th-level-ua2020spellsandmagictattoos\");\nawait moveFile(\"items\", \"spellwrought-tattoo-cantrip\", \"spellwrought-tattoo-cantrip-ua2020spellsandmagictattoos\");\nawait moveFile(\"items\", \"spider-staff\", \"spider-staff-lmop\");\nawait moveFile(\"items\", \"spies-murmur\", \"spies-murmur-ggr\");\nawait moveFile(\"items\", \"spiked-armor\", \"spiked-armor-scag\");\nawait moveFile(\"items\", \"spyglass-of-clairvoyance\", \"spyglass-of-clairvoyance-ai\");\nawait moveFile(\"items\", \"squid-ship\", \"squid-ship-aag\");\nawait moveFile(\"items\", \"staff-of-adornment\", \"staff-of-adornment-xge\");\nawait moveFile(\"items\", \"staff-of-birdcalls\", \"staff-of-birdcalls-xge\");\nawait moveFile(\"items\", \"staff-of-defense\", \"staff-of-defense-lmop\");\nawait moveFile(\"items\", \"staff-of-dunamancy\", \"staff-of-dunamancy-egw\");\nawait moveFile(\"items\", \"staff-of-fate\", \"staff-of-fate-cm\");\nawait moveFile(\"items\", \"staff-of-flowers\", \"staff-of-flowers-xge\");\nawait moveFile(\"items\", \"staff-of-the-forgotten-one\", \"staff-of-the-forgotten-one-toa\");\nawait moveFile(\"items\", \"staff-of-the-ivory-claw\", \"staff-of-the-ivory-claw-egw\");\nawait moveFile(\"items\", \"star-moth\", \"star-moth-aag\");\nawait moveFile(\"items\", \"statuette-of-saint-markovia\", \"statuette-of-saint-markovia-cos\");\nawait moveFile(\"items\", \"steel\", \"steel-wbtw\");\nawait moveFile(\"items\", \"stirring-dragon-touched-focus\", \"stirring-dragon-touched-focus-ftd\");\nawait moveFile(\"items\", \"stirring-dragon-vessel\", \"stirring-dragon-vessel-ftd\");\nawait moveFile(\"items\", \"stirring-scaled-ornament\", \"stirring-scaled-ornament-ftd\");\nawait moveFile(\"items\", \"stone-of-creation\", \"stone-of-creation-aitfr-avt\");\nawait moveFile(\"items\", \"stone-of-golorr\", \"stone-of-golorr-wdh\");\nawait moveFile(\"items\", \"stone-of-ill-luck\", \"stone-of-ill-luck-tftyp\");\nawait moveFile(\"items\", \"stonespeaker-crystal\", \"stonespeaker-crystal-oota\");\nawait moveFile(\"items\", \"stonkys-ring\", \"stonkys-ring-cm\");\nawait moveFile(\"items\", \"storm-boomerang\", \"storm-boomerang-pota\");\nawait moveFile(\"items\", \"stormgirdle-awakened\", \"stormgirdle-awakened-egw\");\nawait moveFile(\"items\", \"stormgirdle-dormant\", \"stormgirdle-dormant-egw\");\nawait moveFile(\"items\", \"stormgirdle-exalted\", \"stormgirdle-exalted-egw\");\nawait moveFile(\"items\", \"strixhaven-pennant\", \"strixhaven-pennant-scc\");\nawait moveFile(\"items\", \"sun-amulet-on-a-beaded-chain-sun-empire\", \"sun-amulet-on-a-beaded-chain-sun-empire-psx\");\nawait moveFile(\"items\", \"sun\", \"sun-wdh\");\nawait moveFile(\"items\", \"sunforger\", \"sunforger-ggr\");\nawait moveFile(\"items\", \"sunsword\", \"sunsword-cos\");\nawait moveFile(\"items\", \"survival-mantle\", \"survival-mantle-vgm\");\nawait moveFile(\"items\", \"sword-of-the-paruns\", \"sword-of-the-paruns-ggr\");\nawait moveFile(\"items\", \"sword-of-zariel\", \"sword-of-zariel-bgdia\");\nawait moveFile(\"items\", \"talking-doll\", \"talking-doll-xge\");\nawait moveFile(\"items\", \"talarith\", \"talarith-bam\");\nawait moveFile(\"items\", \"tangler-grenade\", \"tangler-grenade-wdmm\");\nawait moveFile(\"items\", \"tankard-of-plenty\", \"tankard-of-plenty-hotdq\");\nawait moveFile(\"items\", \"tankard-of-sobriety\", \"tankard-of-sobriety-xge\");\nawait moveFile(\"items\", \"tantan\", \"tantan-scag\");\nawait moveFile(\"items\", \"taol\", \"taol-wdh\");\nawait moveFile(\"items\", \"tearulai\", \"tearulai-wdmm\");\nawait moveFile(\"items\", \"teeth-of-dahlver-nar\", \"teeth-of-dahlver-nar-tce\");\nawait moveFile(\"items\", \"tej\", \"tej-toa\");\nawait moveFile(\"items\", \"teleportation-tablet\", \"teleportation-tablet-crcotn\");\nawait moveFile(\"items\", \"teleporter-ring\", \"teleporter-ring-wdh\");\nawait moveFile(\"items\", \"the-bloody-end-awakened\", \"the-bloody-end-awakened-egw\");\nawait moveFile(\"items\", \"the-bloody-end-dormant\", \"the-bloody-end-dormant-egw\");\nawait moveFile(\"items\", \"the-bloody-end-exalted\", \"the-bloody-end-exalted-egw\");\nawait moveFile(\"items\", \"the-codicil-of-white\", \"the-codicil-of-white-idrotf\");\nawait moveFile(\"items\", \"the-incantations-of-iriolarthas\", \"the-incantations-of-iriolarthas-idrotf\");\nawait moveFile(\"items\", \"the-infernal-machine-of-lum-the-mad\", \"the-infernal-machine-of-lum-the-mad-imr\");\nawait moveFile(\"items\", \"theki-root\", \"theki-root-egw\");\nawait moveFile(\"items\", \"thelarr\", \"thelarr-scag\");\nawait moveFile(\"items\", \"thermal-cube\", \"thermal-cube-idrotf\");\nawait moveFile(\"items\", \"thessaltoxin-antidote\", \"thessaltoxin-antidote-imr\");\nawait moveFile(\"items\", \"thessaltoxin\", \"thessaltoxin-imr\");\nawait moveFile(\"items\", \"thunder-absorbing-tattoo\", \"thunder-absorbing-tattoo-tce\");\nawait moveFile(\"items\", \"timepiece-of-travel\", \"timepiece-of-travel-ai\");\nawait moveFile(\"items\", \"tinderstrike\", \"tinderstrike-pota\");\nawait moveFile(\"items\", \"tiny-jade-figurine-river-heralds\", \"tiny-jade-figurine-river-heralds-psx\");\nawait moveFile(\"items\", \"tocken\", \"tocken-scag\");\nawait moveFile(\"items\", \"tome-of-strahd\", \"tome-of-strahd-cos\");\nawait moveFile(\"items\", \"topaz-annihilator\", \"topaz-annihilator-ftd\");\nawait moveFile(\"items\", \"travel-alchemical-kit\", \"travel-alchemical-kit-ai\");\nawait moveFile(\"items\", \"treasure-chest-crafted-of-exotic-wood-with-gold-fittings-and-opals-brazen-coalition\", \"treasure-chest-crafted-of-exotic-wood-with-gold-fittings-and-opals-brazen-coalition-psx\");\nawait moveFile(\"items\", \"treebane\", \"treebane-cos\");\nawait moveFile(\"items\", \"turtle-ship\", \"turtle-ship-aag\");\nawait moveFile(\"items\", \"two-birds-sling\", \"two-birds-sling-mot\");\nawait moveFile(\"items\", \"tyrant-ship\", \"tyrant-ship-aag\");\nawait moveFile(\"items\", \"unbreakable-arrow\", \"unbreakable-arrow-xge\");\nawait moveFile(\"items\", \"uncommon-glamerweave\", \"uncommon-glamerweave-erlw\");\nawait moveFile(\"items\", \"vanquishers-banner\", \"vanquishers-banner-xmts\");\nawait moveFile(\"items\", \"vanraks-mithral-shirt\", \"vanraks-mithral-shirt-wdmm\");\nawait moveFile(\"items\", \"velvet-doublet-with-gold-buttons-legion-of-dusk\", \"velvet-doublet-with-gold-buttons-legion-of-dusk-psx\");\nawait moveFile(\"items\", \"ventilating-lungs\", \"ventilating-lungs-erlw\");\nawait moveFile(\"items\", \"verminshroud-awakened\", \"verminshroud-awakened-egw\");\nawait moveFile(\"items\", \"verminshroud-dormant\", \"verminshroud-dormant-egw\");\nawait moveFile(\"items\", \"verminshroud-exalted\", \"verminshroud-exalted-egw\");\nawait moveFile(\"items\", \"veterans-cane\", \"veterans-cane-xge\");\nawait moveFile(\"items\", \"vial-of-stardust\", \"vial-of-stardust-wdmm\");\nawait moveFile(\"items\", \"vial-of-thought-capture\", \"vial-of-thought-capture-azfyt\");\nawait moveFile(\"items\", \"voting-kit\", \"voting-kit-ai\");\nawait moveFile(\"items\", \"vox-seeker\", \"vox-seeker-egw\");\nawait moveFile(\"items\", \"voyager-staff\", \"voyager-staff-ggr\");\nawait moveFile(\"items\", \"wakened-dragon-touched-focus\", \"wakened-dragon-touched-focus-ftd\");\nawait moveFile(\"items\", \"wakened-dragon-vessel\", \"wakened-dragon-vessel-ftd\");\nawait moveFile(\"items\", \"wakened-scaled-ornament\", \"wakened-scaled-ornament-ftd\");\nawait moveFile(\"items\", \"wand-of-conducting\", \"wand-of-conducting-xge\");\nawait moveFile(\"items\", \"wand-of-entangle\", \"wand-of-entangle-tftyp\");\nawait moveFile(\"items\", \"wand-of-pyrotechnics\", \"wand-of-pyrotechnics-xge\");\nawait moveFile(\"items\", \"wand-of-scowls\", \"wand-of-scowls-xge\");\nawait moveFile(\"items\", \"wand-of-smiles\", \"wand-of-smiles-xge\");\nawait moveFile(\"items\", \"wand-of-viscid-globs\", \"wand-of-viscid-globs-oota\");\nawait moveFile(\"items\", \"wand-of-winter\", \"wand-of-winter-hotdq\");\nawait moveFile(\"items\", \"wand-sheath\", \"wand-sheath-erlw\");\nawait moveFile(\"items\", \"wargong\", \"wargong-scag\");\nawait moveFile(\"items\", \"wasp-ship\", \"wasp-ship-aag\");\nawait moveFile(\"items\", \"watchful-helm\", \"watchful-helm-cm\");\nawait moveFile(\"items\", \"waythe\", \"waythe-tftyp\");\nawait moveFile(\"items\", \"weird-tank\", \"weird-tank-pota\");\nawait moveFile(\"items\", \"wheel-of-stars\", \"wheel-of-stars-ai\");\nawait moveFile(\"items\", \"wheel-of-wind-and-water\", \"wheel-of-wind-and-water-erlw\");\nawait moveFile(\"items\", \"whisper-jar\", \"whisper-jar-ai\");\nawait moveFile(\"items\", \"white-chromatic-rose\", \"white-chromatic-rose-wbtw\");\nawait moveFile(\"items\", \"white-dragon-cape\", \"white-dragon-cape-tftyp\");\nawait moveFile(\"items\", \"white-dragon-mask\", \"white-dragon-mask-rotos\");\nawait moveFile(\"items\", \"white-ghost-orchid-seed\", \"white-ghost-orchid-seed-jttrc\");\nawait moveFile(\"items\", \"wildroot\", \"wildroot-toa\");\nawait moveFile(\"items\", \"wildspace-orrery\", \"wildspace-orrery-aag\");\nawait moveFile(\"items\", \"will-of-the-talon-awakened\", \"will-of-the-talon-awakened-egw\");\nawait moveFile(\"items\", \"will-of-the-talon-dormant\", \"will-of-the-talon-dormant-egw\");\nawait moveFile(\"items\", \"will-of-the-talon-exalted\", \"will-of-the-talon-exalted-egw\");\nawait moveFile(\"items\", \"willowshade-oil\", \"willowshade-oil-egw\");\nawait moveFile(\"items\", \"windvane\", \"windvane-pota\");\nawait moveFile(\"items\", \"wingwear\", \"wingwear-pota\");\nawait moveFile(\"items\", \"winters-dark-bite\", \"winters-dark-bite-hftt\");\nawait moveFile(\"items\", \"witchlight-vane\", \"witchlight-vane-wbtw\");\nawait moveFile(\"items\", \"witchlight-watch\", \"witchlight-watch-wbtw\");\nawait moveFile(\"items\", \"witherbloom-primer\", \"witherbloom-primer-scc\");\nawait moveFile(\"items\", \"woodcutters-axe\", \"woodcutters-axe-wbtw\");\nawait moveFile(\"items\", \"wreath-of-the-prism-awakened\", \"wreath-of-the-prism-awakened-egw\");\nawait moveFile(\"items\", \"wreath-of-the-prism-dormant\", \"wreath-of-the-prism-dormant-egw\");\nawait moveFile(\"items\", \"wreath-of-the-prism-exalted\", \"wreath-of-the-prism-exalted-egw\");\nawait moveFile(\"items\", \"wyllows-staff-of-flowers\", \"wyllows-staff-of-flowers-wdmm\");\nawait moveFile(\"items\", \"wyrmskull-throne\", \"wyrmskull-throne-skt\");\nawait moveFile(\"items\", \"yahcha\", \"yahcha-toa\");\nawait moveFile(\"items\", \"yarting\", \"yarting-scag\");\nawait moveFile(\"items\", \"yklwa\", \"yklwa-toa\");\nawait moveFile(\"items\", \"ythryn-mythallar\", \"ythryn-mythallar-idrotf\");\nawait moveFile(\"items\", \"zabou\", \"zabou-toa\");\nawait moveFile(\"items\", \"zulkoon\", \"zulkoon-scag\");\nawait moveFile(\"races\", \"aarakocra\", \"aarakocra-mpmm\");\nawait moveFile(\"races\", \"aasimar-fallen\", \"aasimar-fallen-vgm\");\nawait moveFile(\"races\", \"aasimar\", \"aasimar-mpmm\");\nawait moveFile(\"races\", \"aasimar-protector\", \"aasimar-protector-vgm\");\nawait moveFile(\"races\", \"aasimar-scourge\", \"aasimar-scourge-vgm\");\nawait moveFile(\"races\", \"aetherborn\", \"aetherborn-psk\");\nawait moveFile(\"races\", \"astral-elf\", \"astral-elf-aag\");\nawait moveFile(\"races\", \"astral-elf-ua\", \"astral-elf-ua2021travelersofthemultiverse\");\nawait moveFile(\"races\", \"autognome\", \"autognome-aag\");\nawait moveFile(\"races\", \"autognome-ua\", \"autognome-ua2021travelersofthemultiverse\");\nawait moveFile(\"races\", \"aven-hawk-headed\", \"aven-hawk-headed-psa\");\nawait moveFile(\"races\", \"aven-ibis-headed\", \"aven-ibis-headed-psa\");\nawait moveFile(\"races\", \"aven\", \"aven-psa\");\nawait moveFile(\"races\", \"bugbear\", \"bugbear-mpmm\");\nawait moveFile(\"races\", \"centaur\", \"centaur-mpmm\");\nawait moveFile(\"races\", \"centaur-ua\", \"centaur-uacentaursminotaurs\");\nawait moveFile(\"races\", \"changeling\", \"changeling-mpmm\");\nawait moveFile(\"races\", \"changeling-ua\", \"changeling-uaeberron\");\nawait moveFile(\"races\", \"custom-lineage\", \"custom-lineage-tce\");\nawait moveFile(\"races\", \"deep-gnome\", \"deep-gnome-mpmm\");\nawait moveFile(\"races\", \"dhampir-ua\", \"dhampir-ua2021gothiclineages\");\nawait moveFile(\"races\", \"dhampir\", \"dhampir-vrgr\");\nawait moveFile(\"races\", \"dragonborn-chromatic\", \"dragonborn-chromatic-ftd\");\nawait moveFile(\"races\", \"dragonborn-chromatic-ua\", \"dragonborn-chromatic-ua2021draconicoptions\");\nawait moveFile(\"races\", \"dragonborn-draconblood\", \"dragonborn-draconblood-egw\");\nawait moveFile(\"races\", \"dragonborn-gem\", \"dragonborn-gem-ftd\");\nawait moveFile(\"races\", \"dragonborn-gem-ua\", \"dragonborn-gem-ua2021draconicoptions\");\nawait moveFile(\"races\", \"dragonborn-metallic\", \"dragonborn-metallic-ftd\");\nawait moveFile(\"races\", \"dragonborn-metallic-ua\", \"dragonborn-metallic-ua2021draconicoptions\");\nawait moveFile(\"races\", \"dragonborn-ravenite\", \"dragonborn-ravenite-egw\");\nawait moveFile(\"races\", \"duergar\", \"duergar-mpmm\");\nawait moveFile(\"races\", \"dwarf-kaladesh\", \"dwarf-kaladesh-psk\");\nawait moveFile(\"races\", \"dwarf-mark-of-warding\", \"dwarf-mark-of-warding-erlw\");\nawait moveFile(\"races\", \"eladrin\", \"eladrin-mpmm\");\nawait moveFile(\"races\", \"elf-avariel-ua\", \"elf-avariel-uaelfsubraces\");\nawait moveFile(\"races\", \"elf-eladrin-ua\", \"elf-eladrin-uaeladrinandgith\");\nawait moveFile(\"races\", \"elf-grugach-ua\", \"elf-grugach-uaelfsubraces\");\nawait moveFile(\"races\", \"elf-high-aereni\", \"elf-high-aereni-uawge\");\nawait moveFile(\"races\", \"elf-high-valenar\", \"elf-high-valenar-uawge\");\nawait moveFile(\"races\", \"elf-kaladesh\", \"elf-kaladesh-psk\");\nawait moveFile(\"races\", \"elf-mark-of-shadow\", \"elf-mark-of-shadow-erlw\");\nawait moveFile(\"races\", \"elf-pallid\", \"elf-pallid-egw\");\nawait moveFile(\"races\", \"elf-sea-ua\", \"elf-sea-uaelfsubraces\");\nawait moveFile(\"races\", \"elf-shadar-kai-ua\", \"elf-shadar-kai-uaelfsubraces\");\nawait moveFile(\"races\", \"elf-wood-aereni\", \"elf-wood-aereni-uawge\");\nawait moveFile(\"races\", \"elf-wood-valenar\", \"elf-wood-valenar-uawge\");\nawait moveFile(\"races\", \"elf-zendikar\", \"elf-zendikar-psz\");\nawait moveFile(\"races\", \"fairy\", \"fairy-mpmm\");\nawait moveFile(\"races\", \"fairy-ua\", \"fairy-ua2021folkofthefeywild\");\nawait moveFile(\"races\", \"firbolg\", \"firbolg-mpmm\");\nawait moveFile(\"races\", \"genasi-air\", \"genasi-air-mpmm\");\nawait moveFile(\"races\", \"genasi-earth\", \"genasi-earth-mpmm\");\nawait moveFile(\"races\", \"genasi-fire\", \"genasi-fire-mpmm\");\nawait moveFile(\"races\", \"genasi\", \"genasi-mpmm\");\nawait moveFile(\"races\", \"genasi-water\", \"genasi-water-mpmm\");\nawait moveFile(\"races\", \"giff\", \"giff-aag\");\nawait moveFile(\"races\", \"giff-ua\", \"giff-ua2021travelersofthemultiverse\");\nawait moveFile(\"races\", \"gith-githyanki-ua\", \"gith-githyanki-uaeladrinandgith\");\nawait moveFile(\"races\", \"gith-githzerai-ua\", \"gith-githzerai-uaeladrinandgith\");\nawait moveFile(\"races\", \"gith\", \"gith-mtf\");\nawait moveFile(\"races\", \"gith-ua\", \"gith-uaeladrinandgith\");\nawait moveFile(\"races\", \"githyanki\", \"githyanki-mpmm\");\nawait moveFile(\"races\", \"githzerai\", \"githzerai-mpmm\");\nawait moveFile(\"races\", \"glitchling-ua\", \"glitchling-ua2022wondersofthemultiverse\");\nawait moveFile(\"races\", \"gnome-mark-of-scribing\", \"gnome-mark-of-scribing-erlw\");\nawait moveFile(\"races\", \"goblin-ixalan\", \"goblin-ixalan-psx\");\nawait moveFile(\"races\", \"goblin\", \"goblin-mpmm\");\nawait moveFile(\"races\", \"goblin-zendikar\", \"goblin-zendikar-psz\");\nawait moveFile(\"races\", \"goliath\", \"goliath-mpmm\");\nawait moveFile(\"races\", \"grung\", \"grung-oga\");\nawait moveFile(\"races\", \"hadozee\", \"hadozee-aag\");\nawait moveFile(\"races\", \"hadozee-ua\", \"hadozee-ua2021travelersofthemultiverse\");\nawait moveFile(\"races\", \"half-elf-aquatic-elf-descent\", \"half-elf-aquatic-elf-descent-scag\");\nawait moveFile(\"races\", \"half-elf-drow-descent\", \"half-elf-drow-descent-scag\");\nawait moveFile(\"races\", \"half-elf-mark-of-detection\", \"half-elf-mark-of-detection-erlw\");\nawait moveFile(\"races\", \"half-elf-mark-of-storm\", \"half-elf-mark-of-storm-erlw\");\nawait moveFile(\"races\", \"half-elf-moon-elf-or-sun-elf-descent\", \"half-elf-moon-elf-or-sun-elf-descent-scag\");\nawait moveFile(\"races\", \"half-elf-wood-elf-descent\", \"half-elf-wood-elf-descent-scag\");\nawait moveFile(\"races\", \"half-orc-mark-of-finding\", \"half-orc-mark-of-finding-erlw\");\nawait moveFile(\"races\", \"halfling-ghostwise\", \"halfling-ghostwise-scag\");\nawait moveFile(\"races\", \"halfling-lotusden\", \"halfling-lotusden-egw\");\nawait moveFile(\"races\", \"halfling-mark-of-healing\", \"halfling-mark-of-healing-erlw\");\nawait moveFile(\"races\", \"halfling-mark-of-hospitality\", \"halfling-mark-of-hospitality-erlw\");\nawait moveFile(\"races\", \"harengon\", \"harengon-mpmm\");\nawait moveFile(\"races\", \"hexblood-ua\", \"hexblood-ua2021gothiclineages\");\nawait moveFile(\"races\", \"hexblood\", \"hexblood-vrgr\");\nawait moveFile(\"races\", \"hobgoblin\", \"hobgoblin-mpmm\");\nawait moveFile(\"races\", \"hobgoblin-ua\", \"hobgoblin-ua2021folkofthefeywild\");\nawait moveFile(\"races\", \"human-amonkhet\", \"human-amonkhet-psa\");\nawait moveFile(\"races\", \"human-innistrad\", \"human-innistrad-psi\");\nawait moveFile(\"races\", \"human-ixalan\", \"human-ixalan-psx\");\nawait moveFile(\"races\", \"human-keldon\", \"human-keldon-psd\");\nawait moveFile(\"races\", \"human-mark-of-finding\", \"human-mark-of-finding-erlw\");\nawait moveFile(\"races\", \"human-mark-of-handling\", \"human-mark-of-handling-erlw\");\nawait moveFile(\"races\", \"human-mark-of-making\", \"human-mark-of-making-erlw\");\nawait moveFile(\"races\", \"human-mark-of-passage\", \"human-mark-of-passage-erlw\");\nawait moveFile(\"races\", \"human-mark-of-sentinel\", \"human-mark-of-sentinel-erlw\");\nawait moveFile(\"races\", \"human-zendikar\", \"human-zendikar-psz\");\nawait moveFile(\"races\", \"kalashtar\", \"kalashtar-erlw\");\nawait moveFile(\"races\", \"kalashtar-ua\", \"kalashtar-uaracesofeberron\");\nawait moveFile(\"races\", \"kender-ua\", \"kender-ua2022heroesofkrynn\");\nawait moveFile(\"races\", \"kenku\", \"kenku-mpmm\");\nawait moveFile(\"races\", \"khenra\", \"khenra-psa\");\nawait moveFile(\"races\", \"kobold\", \"kobold-mpmm\");\nawait moveFile(\"races\", \"kobold-ua\", \"kobold-ua2021draconicoptions\");\nawait moveFile(\"races\", \"kor\", \"kor-psz\");\nawait moveFile(\"races\", \"leonin\", \"leonin-mot\");\nawait moveFile(\"races\", \"lizardfolk\", \"lizardfolk-mpmm\");\nawait moveFile(\"races\", \"locathah\", \"locathah-lr\");\nawait moveFile(\"races\", \"loxodon\", \"loxodon-ggr\");\nawait moveFile(\"races\", \"loxodon-ua\", \"loxodon-uaracesofravnica\");\nawait moveFile(\"races\", \"merfolk-ixalan\", \"merfolk-ixalan-psx\");\nawait moveFile(\"races\", \"merfolk-zendikar\", \"merfolk-zendikar-psz\");\nawait moveFile(\"races\", \"minotaur-amonkhet\", \"minotaur-amonkhet-psa\");\nawait moveFile(\"races\", \"minotaur-krynn-ua\", \"minotaur-krynn-uawaterborneadventures\");\nawait moveFile(\"races\", \"minotaur\", \"minotaur-mpmm\");\nawait moveFile(\"races\", \"minotaur-ua\", \"minotaur-uacentaursminotaurs\");\nawait moveFile(\"races\", \"naga\", \"naga-psa\");\nawait moveFile(\"races\", \"orc-ixalan\", \"orc-ixalan-psx\");\nawait moveFile(\"races\", \"orc\", \"orc-mpmm\");\nawait moveFile(\"races\", \"owlfolk-ua\", \"owlfolk-ua2021folkofthefeywild\");\nawait moveFile(\"races\", \"owlin\", \"owlin-scc\");\nawait moveFile(\"races\", \"plasmoid\", \"plasmoid-aag\");\nawait moveFile(\"races\", \"plasmoid-ua\", \"plasmoid-ua2021travelersofthemultiverse\");\nawait moveFile(\"races\", \"rabbitfolk-ua\", \"rabbitfolk-ua2021folkofthefeywild\");\nawait moveFile(\"races\", \"reborn-ua\", \"reborn-ua2021gothiclineages\");\nawait moveFile(\"races\", \"reborn\", \"reborn-vrgr\");\nawait moveFile(\"races\", \"revenant-ua\", \"revenant-uagothicheroes\");\nawait moveFile(\"races\", \"satyr\", \"satyr-mpmm\");\nawait moveFile(\"races\", \"sea-elf\", \"sea-elf-mpmm\");\nawait moveFile(\"races\", \"shadar-kai\", \"shadar-kai-mpmm\");\nawait moveFile(\"races\", \"shifter-beasthide\", \"shifter-beasthide-erlw\");\nawait moveFile(\"races\", \"shifter-beasthide-ua\", \"shifter-beasthide-uaracesofeberron\");\nawait moveFile(\"races\", \"shifter-cliffwalk-ua\", \"shifter-cliffwalk-uaeberron\");\nawait moveFile(\"races\", \"shifter-longstride-ua\", \"shifter-longstride-uaeberron\");\nawait moveFile(\"races\", \"shifter-longtooth\", \"shifter-longtooth-erlw\");\nawait moveFile(\"races\", \"shifter-longtooth-ua\", \"shifter-longtooth-uaracesofeberron\");\nawait moveFile(\"races\", \"shifter\", \"shifter-mpmm\");\nawait moveFile(\"races\", \"shifter-razorclaw-ua\", \"shifter-razorclaw-uaeberron\");\nawait moveFile(\"races\", \"shifter-swiftstride\", \"shifter-swiftstride-erlw\");\nawait moveFile(\"races\", \"shifter-swiftstride-ua\", \"shifter-swiftstride-uaracesofeberron\");\nawait moveFile(\"races\", \"shifter-ua\", \"shifter-uaeberron\");\nawait moveFile(\"races\", \"shifter-wildhunt\", \"shifter-wildhunt-erlw\");\nawait moveFile(\"races\", \"shifter-wildhunt-ua\", \"shifter-wildhunt-uaeberron\");\nawait moveFile(\"races\", \"simic-hybrid\", \"simic-hybrid-ggr\");\nawait moveFile(\"races\", \"simic-hybrid-ua\", \"simic-hybrid-uaracesofravnica\");\nawait moveFile(\"races\", \"siren\", \"siren-psx\");\nawait moveFile(\"races\", \"tabaxi\", \"tabaxi-mpmm\");\nawait moveFile(\"races\", \"thri-kreen\", \"thri-kreen-aag\");\nawait moveFile(\"races\", \"thri-kreen-ua\", \"thri-kreen-ua2021travelersofthemultiverse\");\nawait moveFile(\"races\", \"tiefling-abyssal-ua\", \"tiefling-abyssal-uathatoldblackmagic\");\nawait moveFile(\"races\", \"tiefling-asmodeus-ua\", \"tiefling-asmodeus-uafiendishoptions\");\nawait moveFile(\"races\", \"tiefling-baalzebul-ua\", \"tiefling-baalzebul-uafiendishoptions\");\nawait moveFile(\"races\", \"tiefling-devils-tongue\", \"tiefling-devils-tongue-scag\");\nawait moveFile(\"races\", \"tiefling-dispater-ua\", \"tiefling-dispater-uafiendishoptions\");\nawait moveFile(\"races\", \"tiefling-fierna-ua\", \"tiefling-fierna-uafiendishoptions\");\nawait moveFile(\"races\", \"tiefling-glasya-ua\", \"tiefling-glasya-uafiendishoptions\");\nawait moveFile(\"races\", \"tiefling-hellfire\", \"tiefling-hellfire-scag\");\nawait moveFile(\"races\", \"tiefling-infernal-legacy\", \"tiefling-infernal-legacy-scag\");\nawait moveFile(\"races\", \"tiefling-levistus-ua\", \"tiefling-levistus-uafiendishoptions\");\nawait moveFile(\"races\", \"tiefling-mammon-ua\", \"tiefling-mammon-uafiendishoptions\");\nawait moveFile(\"races\", \"tiefling-mephistopheles-ua\", \"tiefling-mephistopheles-uafiendishoptions\");\nawait moveFile(\"races\", \"tiefling-winged\", \"tiefling-winged-scag\");\nawait moveFile(\"races\", \"tiefling-zariel-ua\", \"tiefling-zariel-uafiendishoptions\");\nawait moveFile(\"races\", \"tortle\", \"tortle-mpmm\");\nawait moveFile(\"races\", \"triton\", \"triton-mpmm\");\nawait moveFile(\"races\", \"vampire-ixalan\", \"vampire-ixalan-psx\");\nawait moveFile(\"races\", \"vampire-zendikar\", \"vampire-zendikar-psz\");\nawait moveFile(\"races\", \"vedalken\", \"vedalken-psk\");\nawait moveFile(\"races\", \"vedalken-ua\", \"vedalken-uaracesofravnica\");\nawait moveFile(\"races\", \"verdan\", \"verdan-ai\");\nawait moveFile(\"races\", \"viashino-ua\", \"viashino-uaracesofravnica\");\nawait moveFile(\"races\", \"warforged-envoy-ua\", \"warforged-envoy-uaracesofeberron\");\nawait moveFile(\"races\", \"warforged\", \"warforged-erlw\");\nawait moveFile(\"races\", \"warforged-juggernaut-ua\", \"warforged-juggernaut-uaracesofeberron\");\nawait moveFile(\"races\", \"warforged-skirmisher-ua\", \"warforged-skirmisher-uaracesofeberron\");\nawait moveFile(\"races\", \"warforged-ua\", \"warforged-uaeberron\");\nawait moveFile(\"races\", \"yuan-ti\", \"yuan-ti-mpmm\");\nawait moveFile(\"spells\", \"abi-dalzims-horrid-wilting\", \"abi-dalzims-horrid-wilting-xge\");\nawait moveFile(\"spells\", \"absorb-elements\", \"absorb-elements-xge\");\nawait moveFile(\"spells\", \"acid-stream\", \"acid-stream-ua2020spellsandmagictattoos\");\nawait moveFile(\"spells\", \"aganazzars-scorcher\", \"aganazzars-scorcher-xge\");\nawait moveFile(\"spells\", \"air-bubble\", \"air-bubble-aag\");\nawait moveFile(\"spells\", \"antagonize\", \"antagonize-ua2022wondersofthemultiverse\");\nawait moveFile(\"spells\", \"arcane-hacking-ua\", \"arcane-hacking-ua-uamodernmagic\");\nawait moveFile(\"spells\", \"arcane-weapon\", \"arcane-weapon-uaartificerrevisited\");\nawait moveFile(\"spells\", \"ashardalons-stride\", \"ashardalons-stride-ftd\");\nawait moveFile(\"spells\", \"beast-bond\", \"beast-bond-xge\");\nawait moveFile(\"spells\", \"blade-of-disaster\", \"blade-of-disaster-tce\");\nawait moveFile(\"spells\", \"bones-of-the-earth\", \"bones-of-the-earth-xge\");\nawait moveFile(\"spells\", \"booming-blade\", \"booming-blade-tce\");\nawait moveFile(\"spells\", \"borrowed-knowledge\", \"borrowed-knowledge-scc\");\nawait moveFile(\"spells\", \"catapult\", \"catapult-xge\");\nawait moveFile(\"spells\", \"catnap\", \"catnap-xge\");\nawait moveFile(\"spells\", \"cause-fear-ua\", \"cause-fear-ua-uastarterspells\");\nawait moveFile(\"spells\", \"cause-fear\", \"cause-fear-xge\");\nawait moveFile(\"spells\", \"ceremony-ua\", \"ceremony-ua-uastarterspells\");\nawait moveFile(\"spells\", \"ceremony\", \"ceremony-xge\");\nawait moveFile(\"spells\", \"chaos-bolt-ua\", \"chaos-bolt-ua-uastarterspells\");\nawait moveFile(\"spells\", \"chaos-bolt\", \"chaos-bolt-xge\");\nawait moveFile(\"spells\", \"charm-monster\", \"charm-monster-xge\");\nawait moveFile(\"spells\", \"commune-with-city-ua\", \"commune-with-city-ua-uamodernmagic\");\nawait moveFile(\"spells\", \"conjure-barlgura-ua\", \"conjure-barlgura-ua-uathatoldblackmagic\");\nawait moveFile(\"spells\", \"conjure-hezrou-ua\", \"conjure-hezrou-ua-uathatoldblackmagic\");\nawait moveFile(\"spells\", \"conjure-knowbot-ua\", \"conjure-knowbot-ua-uamodernmagic\");\nawait moveFile(\"spells\", \"conjure-lesser-demon-ua\", \"conjure-lesser-demon-ua-uathatoldblackmagic\");\nawait moveFile(\"spells\", \"conjure-shadow-demon-ua\", \"conjure-shadow-demon-ua-uathatoldblackmagic\");\nawait moveFile(\"spells\", \"conjure-vrock-ua\", \"conjure-vrock-ua-uathatoldblackmagic\");\nawait moveFile(\"spells\", \"control-flames\", \"control-flames-xge\");\nawait moveFile(\"spells\", \"control-winds\", \"control-winds-xge\");\nawait moveFile(\"spells\", \"create-bonfire\", \"create-bonfire-xge\");\nawait moveFile(\"spells\", \"create-homunculus\", \"create-homunculus-xge\");\nawait moveFile(\"spells\", \"create-magen\", \"create-magen-idrotf\");\nawait moveFile(\"spells\", \"create-spelljamming-helm\", \"create-spelljamming-helm-aag\");\nawait moveFile(\"spells\", \"crown-of-stars\", \"crown-of-stars-xge\");\nawait moveFile(\"spells\", \"danse-macabre\", \"danse-macabre-xge\");\nawait moveFile(\"spells\", \"dark-star\", \"dark-star-egw\");\nawait moveFile(\"spells\", \"dawn\", \"dawn-xge\");\nawait moveFile(\"spells\", \"digital-phantom-ua\", \"digital-phantom-ua-uamodernmagic\");\nawait moveFile(\"spells\", \"distort-value\", \"distort-value-ai\");\nawait moveFile(\"spells\", \"draconic-transformation\", \"draconic-transformation-ftd\");\nawait moveFile(\"spells\", \"dragons-breath\", \"dragons-breath-xge\");\nawait moveFile(\"spells\", \"dream-of-the-blue-veil\", \"dream-of-the-blue-veil-tce\");\nawait moveFile(\"spells\", \"druid-grove\", \"druid-grove-xge\");\nawait moveFile(\"spells\", \"dust-devil\", \"dust-devil-xge\");\nawait moveFile(\"spells\", \"earth-tremor\", \"earth-tremor-xge\");\nawait moveFile(\"spells\", \"earthbind\", \"earthbind-xge\");\nawait moveFile(\"spells\", \"ego-whip\", \"ego-whip-uafighterroguewizard\");\nawait moveFile(\"spells\", \"elemental-bane\", \"elemental-bane-xge\");\nawait moveFile(\"spells\", \"encode-thoughts\", \"encode-thoughts-ggr\");\nawait moveFile(\"spells\", \"enemies-abound\", \"enemies-abound-xge\");\nawait moveFile(\"spells\", \"enervation\", \"enervation-xge\");\nawait moveFile(\"spells\", \"erupting-earth\", \"erupting-earth-xge\");\nawait moveFile(\"spells\", \"far-step\", \"far-step-xge\");\nawait moveFile(\"spells\", \"fast-friends\", \"fast-friends-ai\");\nawait moveFile(\"spells\", \"find-greater-steed\", \"find-greater-steed-xge\");\nawait moveFile(\"spells\", \"find-vehicle-ua\", \"find-vehicle-ua-uamodernmagic\");\nawait moveFile(\"spells\", \"fizbans-platinum-shield\", \"fizbans-platinum-shield-ua2021draconicoptions\");\nawait moveFile(\"spells\", \"flame-arrows\", \"flame-arrows-xge\");\nawait moveFile(\"spells\", \"flame-stride\", \"flame-stride-ua2021draconicoptions\");\nawait moveFile(\"spells\", \"flock-of-familiars\", \"flock-of-familiars-llk\");\nawait moveFile(\"spells\", \"fortunes-favor\", \"fortunes-favor-egw\");\nawait moveFile(\"spells\", \"frost-fingers\", \"frost-fingers-idrotf\");\nawait moveFile(\"spells\", \"frostbite\", \"frostbite-xge\");\nawait moveFile(\"spells\", \"galders-speedy-courier\", \"galders-speedy-courier-llk\");\nawait moveFile(\"spells\", \"galders-tower\", \"galders-tower-llk\");\nawait moveFile(\"spells\", \"gift-of-alacrity\", \"gift-of-alacrity-egw\");\nawait moveFile(\"spells\", \"gift-of-gab\", \"gift-of-gab-ai\");\nawait moveFile(\"spells\", \"gravity-fissure\", \"gravity-fissure-egw\");\nawait moveFile(\"spells\", \"gravity-sinkhole\", \"gravity-sinkhole-egw\");\nawait moveFile(\"spells\", \"green-flame-blade\", \"green-flame-blade-tce\");\nawait moveFile(\"spells\", \"guardian-of-nature\", \"guardian-of-nature-xge\");\nawait moveFile(\"spells\", \"guiding-hand-ua\", \"guiding-hand-ua-uastarterspells\");\nawait moveFile(\"spells\", \"gust\", \"gust-xge\");\nawait moveFile(\"spells\", \"hand-of-radiance-ua\", \"hand-of-radiance-ua-uastarterspells\");\nawait moveFile(\"spells\", \"haywire-ua\", \"haywire-ua-uamodernmagic\");\nawait moveFile(\"spells\", \"healing-elixir-ua\", \"healing-elixir-ua-uastarterspells\");\nawait moveFile(\"spells\", \"healing-spirit\", \"healing-spirit-xge\");\nawait moveFile(\"spells\", \"holy-weapon\", \"holy-weapon-xge\");\nawait moveFile(\"spells\", \"house-of-cards\", \"house-of-cards-ua2022wondersofthemultiverse\");\nawait moveFile(\"spells\", \"ice-knife\", \"ice-knife-xge\");\nawait moveFile(\"spells\", \"icingdeaths-frost\", \"icingdeaths-frost-ua2021draconicoptions\");\nawait moveFile(\"spells\", \"id-insinuation\", \"id-insinuation-uafighterroguewizard\");\nawait moveFile(\"spells\", \"illusory-dragon\", \"illusory-dragon-xge\");\nawait moveFile(\"spells\", \"immolation\", \"immolation-xge\");\nawait moveFile(\"spells\", \"immovable-object\", \"immovable-object-egw\");\nawait moveFile(\"spells\", \"incite-greed\", \"incite-greed-ai\");\nawait moveFile(\"spells\", \"infallible-relay-ua\", \"infallible-relay-ua-uamodernmagic\");\nawait moveFile(\"spells\", \"infernal-calling\", \"infernal-calling-xge\");\nawait moveFile(\"spells\", \"infestation-ua\", \"infestation-ua-uastarterspells\");\nawait moveFile(\"spells\", \"infestation\", \"infestation-xge\");\nawait moveFile(\"spells\", \"intellect-fortress\", \"intellect-fortress-tce\");\nawait moveFile(\"spells\", \"investiture-of-flame\", \"investiture-of-flame-xge\");\nawait moveFile(\"spells\", \"investiture-of-ice\", \"investiture-of-ice-xge\");\nawait moveFile(\"spells\", \"investiture-of-stone\", \"investiture-of-stone-xge\");\nawait moveFile(\"spells\", \"investiture-of-wind\", \"investiture-of-wind-xge\");\nawait moveFile(\"spells\", \"invisibility-to-cameras-ua\", \"invisibility-to-cameras-ua-uamodernmagic\");\nawait moveFile(\"spells\", \"invulnerability\", \"invulnerability-xge\");\nawait moveFile(\"spells\", \"jims-glowing-coin\", \"jims-glowing-coin-ai\");\nawait moveFile(\"spells\", \"jims-magic-missile\", \"jims-magic-missile-ai\");\nawait moveFile(\"spells\", \"kinetic-jaunt\", \"kinetic-jaunt-scc\");\nawait moveFile(\"spells\", \"life-transference\", \"life-transference-xge\");\nawait moveFile(\"spells\", \"lightning-lure\", \"lightning-lure-tce\");\nawait moveFile(\"spells\", \"linked-glyphs\", \"linked-glyphs-aitfr-avt\");\nawait moveFile(\"spells\", \"maddening-darkness\", \"maddening-darkness-xge\");\nawait moveFile(\"spells\", \"maelstrom\", \"maelstrom-xge\");\nawait moveFile(\"spells\", \"magic-stone\", \"magic-stone-xge\");\nawait moveFile(\"spells\", \"magnify-gravity\", \"magnify-gravity-egw\");\nawait moveFile(\"spells\", \"mass-polymorph\", \"mass-polymorph-xge\");\nawait moveFile(\"spells\", \"maximilians-earthen-grasp\", \"maximilians-earthen-grasp-xge\");\nawait moveFile(\"spells\", \"melfs-minute-meteors\", \"melfs-minute-meteors-xge\");\nawait moveFile(\"spells\", \"mental-barrier\", \"mental-barrier-uafighterroguewizard\");\nawait moveFile(\"spells\", \"mental-prison\", \"mental-prison-xge\");\nawait moveFile(\"spells\", \"mighty-fortress\", \"mighty-fortress-xge\");\nawait moveFile(\"spells\", \"mind-sliver\", \"mind-sliver-tce\");\nawait moveFile(\"spells\", \"mind-spike\", \"mind-spike-xge\");\nawait moveFile(\"spells\", \"mind-thrust\", \"mind-thrust-ua2020psionicoptionsrevisited\");\nawait moveFile(\"spells\", \"mold-earth\", \"mold-earth-xge\");\nawait moveFile(\"spells\", \"motivational-speech\", \"motivational-speech-ai\");\nawait moveFile(\"spells\", \"nathairs-mischief\", \"nathairs-mischief-ftd\");\nawait moveFile(\"spells\", \"negative-energy-flood\", \"negative-energy-flood-xge\");\nawait moveFile(\"spells\", \"on-off-ua\", \"on-off-ua-uamodernmagic\");\nawait moveFile(\"spells\", \"otherworldly-form\", \"otherworldly-form-ua2020spellsandmagictattoos\");\nawait moveFile(\"spells\", \"power-word-pain\", \"power-word-pain-xge\");\nawait moveFile(\"spells\", \"primal-savagery-ua\", \"primal-savagery-ua-uastarterspells\");\nawait moveFile(\"spells\", \"primal-savagery\", \"primal-savagery-xge\");\nawait moveFile(\"spells\", \"primordial-ward\", \"primordial-ward-xge\");\nawait moveFile(\"spells\", \"protection-from-ballistics-ua\", \"protection-from-ballistics-ua-uamodernmagic\");\nawait moveFile(\"spells\", \"psionic-blast\", \"psionic-blast-uafighterroguewizard\");\nawait moveFile(\"spells\", \"psychic-crush\", \"psychic-crush-uafighterroguewizard\");\nawait moveFile(\"spells\", \"psychic-scream\", \"psychic-scream-xge\");\nawait moveFile(\"spells\", \"pulse-wave\", \"pulse-wave-egw\");\nawait moveFile(\"spells\", \"puppet-ua\", \"puppet-ua-uastarterspells\");\nawait moveFile(\"spells\", \"pyrotechnics\", \"pyrotechnics-xge\");\nawait moveFile(\"spells\", \"raulothims-psychic-lance\", \"raulothims-psychic-lance-ua2021draconicoptions\");\nawait moveFile(\"spells\", \"ravenous-void\", \"ravenous-void-egw\");\nawait moveFile(\"spells\", \"reality-break\", \"reality-break-egw\");\nawait moveFile(\"spells\", \"remote-access-ua\", \"remote-access-ua-uamodernmagic\");\nawait moveFile(\"spells\", \"rimes-binding-ice\", \"rimes-binding-ice-ftd\");\nawait moveFile(\"spells\", \"sapping-sting\", \"sapping-sting-egw\");\nawait moveFile(\"spells\", \"scatter\", \"scatter-xge\");\nawait moveFile(\"spells\", \"sense-emotion-ua\", \"sense-emotion-ua-uastarterspells\");\nawait moveFile(\"spells\", \"shadow-blade\", \"shadow-blade-xge\");\nawait moveFile(\"spells\", \"shadow-of-moil\", \"shadow-of-moil-xge\");\nawait moveFile(\"spells\", \"shape-water\", \"shape-water-xge\");\nawait moveFile(\"spells\", \"shutdown-ua\", \"shutdown-ua-uamodernmagic\");\nawait moveFile(\"spells\", \"sickening-radiance\", \"sickening-radiance-xge\");\nawait moveFile(\"spells\", \"silvery-barbs\", \"silvery-barbs-scc\");\nawait moveFile(\"spells\", \"skill-empowerment\", \"skill-empowerment-xge\");\nawait moveFile(\"spells\", \"skywrite\", \"skywrite-xge\");\nawait moveFile(\"spells\", \"snare-ua\", \"snare-ua-uastarterspells\");\nawait moveFile(\"spells\", \"snare\", \"snare-xge\");\nawait moveFile(\"spells\", \"snillocs-snowball-swarm\", \"snillocs-snowball-swarm-xge\");\nawait moveFile(\"spells\", \"soul-cage\", \"soul-cage-xge\");\nawait moveFile(\"spells\", \"spirit-of-death\", \"spirit-of-death-ua2022wondersofthemultiverse\");\nawait moveFile(\"spells\", \"spirit-shroud\", \"spirit-shroud-ua2020spellsandmagictattoos\");\nawait moveFile(\"spells\", \"spray-of-cards\", \"spray-of-cards-ua2022wondersofthemultiverse\");\nawait moveFile(\"spells\", \"steel-wind-strike\", \"steel-wind-strike-xge\");\nawait moveFile(\"spells\", \"storm-sphere\", \"storm-sphere-xge\");\nawait moveFile(\"spells\", \"sudden-awakening-ua\", \"sudden-awakening-ua-uastarterspells\");\nawait moveFile(\"spells\", \"summon-aberrant-spirit\", \"summon-aberrant-spirit-ua2020spellsandmagictattoos\");\nawait moveFile(\"spells\", \"summon-aberration\", \"summon-aberration-tce\");\nawait moveFile(\"spells\", \"summon-beast\", \"summon-beast-tce\");\nawait moveFile(\"spells\", \"summon-bestial-spirit\", \"summon-bestial-spirit-ua2020spellsandmagictattoos\");\nawait moveFile(\"spells\", \"summon-celestial-spirit\", \"summon-celestial-spirit-ua2020spellsandmagictattoos\");\nawait moveFile(\"spells\", \"summon-celestial\", \"summon-celestial-tce\");\nawait moveFile(\"spells\", \"summon-construct\", \"summon-construct-tce\");\nawait moveFile(\"spells\", \"summon-draconic-spirit\", \"summon-draconic-spirit-ftd\");\nawait moveFile(\"spells\", \"summon-elemental-spirit\", \"summon-elemental-spirit-ua2020spellsandmagictattoos\");\nawait moveFile(\"spells\", \"summon-elemental\", \"summon-elemental-tce\");\nawait moveFile(\"spells\", \"summon-fey-spirit\", \"summon-fey-spirit-ua2020spellsandmagictattoos\");\nawait moveFile(\"spells\", \"summon-fey\", \"summon-fey-tce\");\nawait moveFile(\"spells\", \"summon-fiend\", \"summon-fiend-tce\");\nawait moveFile(\"spells\", \"summon-fiendish-spirit\", \"summon-fiendish-spirit-ua2020spellsandmagictattoos\");\nawait moveFile(\"spells\", \"summon-greater-demon\", \"summon-greater-demon-xge\");\nawait moveFile(\"spells\", \"summon-lesser-demons\", \"summon-lesser-demons-xge\");\nawait moveFile(\"spells\", \"summon-shadow-spirit\", \"summon-shadow-spirit-ua2020spellsandmagictattoos\");\nawait moveFile(\"spells\", \"summon-shadowspawn\", \"summon-shadowspawn-tce\");\nawait moveFile(\"spells\", \"summon-undead-spirit\", \"summon-undead-spirit-ua2020spellsandmagictattoos\");\nawait moveFile(\"spells\", \"summon-undead\", \"summon-undead-tce\");\nawait moveFile(\"spells\", \"summon-warrior-spirit\", \"summon-warrior-spirit-ua2022wondersofthemultiverse\");\nawait moveFile(\"spells\", \"sword-burst\", \"sword-burst-tce\");\nawait moveFile(\"spells\", \"synaptic-static\", \"synaptic-static-xge\");\nawait moveFile(\"spells\", \"synchronicity-ua\", \"synchronicity-ua-uamodernmagic\");\nawait moveFile(\"spells\", \"system-backdoor-ua\", \"system-backdoor-ua-uamodernmagic\");\nawait moveFile(\"spells\", \"tashas-caustic-brew\", \"tashas-caustic-brew-tce\");\nawait moveFile(\"spells\", \"tashas-mind-whip\", \"tashas-mind-whip-tce\");\nawait moveFile(\"spells\", \"tashas-otherworldly-guise\", \"tashas-otherworldly-guise-tce\");\nawait moveFile(\"spells\", \"temple-of-the-gods\", \"temple-of-the-gods-xge\");\nawait moveFile(\"spells\", \"temporal-shunt\", \"temporal-shunt-egw\");\nawait moveFile(\"spells\", \"tensers-transformation\", \"tensers-transformation-xge\");\nawait moveFile(\"spells\", \"tether-essence\", \"tether-essence-egw\");\nawait moveFile(\"spells\", \"thought-shield\", \"thought-shield-uafighterroguewizard\");\nawait moveFile(\"spells\", \"thunder-step\", \"thunder-step-xge\");\nawait moveFile(\"spells\", \"thunderclap\", \"thunderclap-xge\");\nawait moveFile(\"spells\", \"tidal-wave\", \"tidal-wave-xge\");\nawait moveFile(\"spells\", \"time-ravage\", \"time-ravage-egw\");\nawait moveFile(\"spells\", \"tiny-servant\", \"tiny-servant-xge\");\nawait moveFile(\"spells\", \"toll-the-dead-ua\", \"toll-the-dead-ua-uastarterspells\");\nawait moveFile(\"spells\", \"toll-the-dead\", \"toll-the-dead-xge\");\nawait moveFile(\"spells\", \"transmute-rock\", \"transmute-rock-xge\");\nawait moveFile(\"spells\", \"unearthly-chorus-ua\", \"unearthly-chorus-ua-uastarterspells\");\nawait moveFile(\"spells\", \"virtue-ua\", \"virtue-ua-uastarterspells\");\nawait moveFile(\"spells\", \"vitriolic-sphere\", \"vitriolic-sphere-xge\");\nawait moveFile(\"spells\", \"vortex-warp\", \"vortex-warp-scc\");\nawait moveFile(\"spells\", \"wall-of-light\", \"wall-of-light-xge\");\nawait moveFile(\"spells\", \"wall-of-sand\", \"wall-of-sand-xge\");\nawait moveFile(\"spells\", \"wall-of-water\", \"wall-of-water-xge\");\nawait moveFile(\"spells\", \"warding-wind\", \"warding-wind-xge\");\nawait moveFile(\"spells\", \"watery-sphere\", \"watery-sphere-xge\");\nawait moveFile(\"spells\", \"whirlwind\", \"whirlwind-xge\");\nawait moveFile(\"spells\", \"wild-cunning-ua\", \"wild-cunning-ua-uastarterspells\");\nawait moveFile(\"spells\", \"wither-and-bloom\", \"wither-and-bloom-scc\");\nawait moveFile(\"spells\", \"word-of-radiance\", \"word-of-radiance-xge\");\nawait moveFile(\"spells\", \"wrath-of-nature\", \"wrath-of-nature-xge\");\nawait moveFile(\"spells\", \"wristpocket\", \"wristpocket-egw\");\nawait moveFile(\"spells\", \"zephyr-strike-ua\", \"zephyr-strike-ua-uastarterspells\");\nawait moveFile(\"spells\", \"zephyr-strike\", \"zephyr-strike-xge\");\n\ntR += \"\\n\"\n%>\n"
  },
  {
    "path": "migration/ttrpg-cli-renameFiles-2.0.0.md",
    "content": "<%*\n// Templater script to rename files in the vault before updating to 2.0.0 CLI output\n// This will move files from 1.x to 2.0.x\n\n// NOTE: There are a lot of files: this process can take awhile\n// It goes faster if you disable sync and plugins that monitor files for changes\n\n// 1. Copy this file into your templates directory\n\n// 2. Update the following paths to match your setup: \n\n// 2a. EDIT Path to the compendium (contains backgrounds, bestiary, etc) from the vault root\nconst compendium = \"/compendium\";\n\n// 2b. EDIT Path to the rules (contains actions, and rule variants) from the vault root\nconst rules = \"/rules\";\n\n// 3. EDIT How many files to rename at a time?\nconst limit = 100;\n\n// 4. Create a new/temporary note, and use the \"Templater: Open Insert Template Modal\"\n// to insert this template into a note. It will update the note content with the files \n// that have been renamed. \n\n\nvar f;\nlet count = 0;\n\ntR += \"| File | New Name |\\n\";\ntR += \"|------|----------|\\n\";\n\nasync function moveFile(path, oldname, newname) {\n    await moveFile(compendium, path, oldname, newname);\n}\nasync function moveRulesFile(path, oldname, newname) {\n    await moveFile(rules, path, oldname, newname);\n}\n\nasync function moveFile(base, path, oldname, newname) {\n    if (count > limit) {\n        return;\n    }\n\n    if (path) {\n        path = path + \"/\";\n    } else {\n        path = \"\";\n    }\n\n    const oldpath = `${base}/${path}${oldname}`\n    const file = await window.app.metadataCache.getFirstLinkpathDest(oldpath, \"\");\n\n    if (file) {\n        const newpath = `${base}/${path}${newname}.md`\n        await this.app.fileManager.renameFile(\n            file,\n            newpath\n        );\n        tR += `| ${oldname} | ${newname} |\\n`;\n\n        count++; // increment counter after moving something\n    }\n}\n\n// 5e files that will be renamed if present\n\nawait moveFile(\"classes\", \"barbarian-path-of-the-ancestral-guardian\", \"barbarian-path-of-the-ancestral-guardian-xge\");\nawait moveFile(\"classes\", \"barbarian-path-of-the-battlerager\", \"barbarian-path-of-the-battlerager-scag\");\nawait moveFile(\"classes\", \"barbarian-path-of-the-beast\", \"barbarian-path-of-the-beast-tce\");\nawait moveFile(\"classes\", \"barbarian-path-of-the-storm-herald\", \"barbarian-path-of-the-storm-herald-xge\");\nawait moveFile(\"classes\", \"barbarian-path-of-the-zealot\", \"barbarian-path-of-the-zealot-xge\");\nawait moveFile(\"classes\", \"barbarian-path-of-wild-magic\", \"barbarian-path-of-wild-magic-tce\");\nawait moveFile(\"classes\", \"bard-college-of-creation\", \"bard-college-of-creation-tce\");\nawait moveFile(\"classes\", \"bard-college-of-eloquence\", \"bard-college-of-eloquence-tce\");\nawait moveFile(\"classes\", \"bard-college-of-glamour\", \"bard-college-of-glamour-xge\");\nawait moveFile(\"classes\", \"bard-college-of-swords\", \"bard-college-of-swords-xge\");\nawait moveFile(\"classes\", \"bard-college-of-whispers\", \"bard-college-of-whispers-xge\");\nawait moveFile(\"classes\", \"cleric-arcana-domain\", \"cleric-arcana-domain-scag\");\nawait moveFile(\"classes\", \"cleric-forge-domain\", \"cleric-forge-domain-xge\");\nawait moveFile(\"classes\", \"cleric-grave-domain\", \"cleric-grave-domain-xge\");\nawait moveFile(\"classes\", \"cleric-order-domain\", \"cleric-order-domain-tce\");\nawait moveFile(\"classes\", \"cleric-peace-domain\", \"cleric-peace-domain-tce\");\nawait moveFile(\"classes\", \"cleric-twilight-domain\", \"cleric-twilight-domain-tce\");\nawait moveFile(\"classes\", \"druid-circle-of-dreams\", \"druid-circle-of-dreams-xge\");\nawait moveFile(\"classes\", \"druid-circle-of-spores\", \"druid-circle-of-spores-tce\");\nawait moveFile(\"classes\", \"druid-circle-of-stars\", \"druid-circle-of-stars-tce\");\nawait moveFile(\"classes\", \"druid-circle-of-the-shepherd\", \"druid-circle-of-the-shepherd-xge\");\nawait moveFile(\"classes\", \"druid-circle-of-wildfire\", \"druid-circle-of-wildfire-tce\");\nawait moveFile(\"classes\", \"fighter-arcane-archer\", \"fighter-arcane-archer-xge\");\nawait moveFile(\"classes\", \"fighter-cavalier\", \"fighter-cavalier-xge\");\nawait moveFile(\"classes\", \"fighter-echo-knight\", \"fighter-echo-knight-egw\");\nawait moveFile(\"classes\", \"fighter-psi-warrior\", \"fighter-psi-warrior-tce\");\nawait moveFile(\"classes\", \"fighter-purple-dragon-knight-banneret\", \"fighter-purple-dragon-knight-banneret-scag\");\nawait moveFile(\"classes\", \"fighter-rune-knight\", \"fighter-rune-knight-tce\");\nawait moveFile(\"classes\", \"fighter-samurai\", \"fighter-samurai-xge\");\nawait moveFile(\"classes\", \"monk-way-of-mercy\", \"monk-way-of-mercy-tce\");\nawait moveFile(\"classes\", \"monk-way-of-the-ascendant-dragon\", \"monk-way-of-the-ascendant-dragon-ftd\");\nawait moveFile(\"classes\", \"monk-way-of-the-astral-self\", \"monk-way-of-the-astral-self-tce\");\nawait moveFile(\"classes\", \"monk-way-of-the-drunken-master\", \"monk-way-of-the-drunken-master-xge\");\nawait moveFile(\"classes\", \"monk-way-of-the-kensei\", \"monk-way-of-the-kensei-xge\");\nawait moveFile(\"classes\", \"monk-way-of-the-long-death\", \"monk-way-of-the-long-death-scag\");\nawait moveFile(\"classes\", \"monk-way-of-the-sun-soul\", \"monk-way-of-the-sun-soul-xge\");\nawait moveFile(\"classes\", \"paladin-oath-of-conquest\", \"paladin-oath-of-conquest-xge\");\nawait moveFile(\"classes\", \"paladin-oath-of-glory\", \"paladin-oath-of-glory-tce\");\nawait moveFile(\"classes\", \"paladin-oath-of-redemption\", \"paladin-oath-of-redemption-xge\");\nawait moveFile(\"classes\", \"paladin-oath-of-the-crown\", \"paladin-oath-of-the-crown-scag\");\nawait moveFile(\"classes\", \"paladin-oath-of-the-watchers\", \"paladin-oath-of-the-watchers-tce\");\nawait moveFile(\"classes\", \"ranger-drakewarden\", \"ranger-drakewarden-ftd\");\nawait moveFile(\"classes\", \"ranger-fey-wanderer\", \"ranger-fey-wanderer-tce\");\nawait moveFile(\"classes\", \"ranger-gloom-stalker\", \"ranger-gloom-stalker-xge\");\nawait moveFile(\"classes\", \"ranger-horizon-walker\", \"ranger-horizon-walker-xge\");\nawait moveFile(\"classes\", \"ranger-monster-slayer\", \"ranger-monster-slayer-xge\");\nawait moveFile(\"classes\", \"ranger-swarmkeeper\", \"ranger-swarmkeeper-tce\");\nawait moveFile(\"classes\", \"rogue-inquisitive\", \"rogue-inquisitive-xge\");\nawait moveFile(\"classes\", \"rogue-mastermind\", \"rogue-mastermind-xge\");\nawait moveFile(\"classes\", \"rogue-phantom\", \"rogue-phantom-tce\");\nawait moveFile(\"classes\", \"rogue-scout\", \"rogue-scout-xge\");\nawait moveFile(\"classes\", \"rogue-soulknife\", \"rogue-soulknife-tce\");\nawait moveFile(\"classes\", \"rogue-swashbuckler\", \"rogue-swashbuckler-xge\");\nawait moveFile(\"classes\", \"sorcerer-aberrant-mind\", \"sorcerer-aberrant-mind-tce\");\nawait moveFile(\"classes\", \"sorcerer-clockwork-soul\", \"sorcerer-clockwork-soul-tce\");\nawait moveFile(\"classes\", \"sorcerer-divine-soul\", \"sorcerer-divine-soul-xge\");\nawait moveFile(\"classes\", \"sorcerer-shadow-magic\", \"sorcerer-shadow-magic-xge\");\nawait moveFile(\"classes\", \"sorcerer-storm-sorcery\", \"sorcerer-storm-sorcery-xge\");\nawait moveFile(\"classes\", \"warlock-the-celestial\", \"warlock-the-celestial-xge\");\nawait moveFile(\"classes\", \"warlock-the-fathomless\", \"warlock-the-fathomless-tce\");\nawait moveFile(\"classes\", \"warlock-the-genie\", \"warlock-the-genie-tce\");\nawait moveFile(\"classes\", \"warlock-the-hexblade\", \"warlock-the-hexblade-xge\");\nawait moveFile(\"classes\", \"warlock-the-undying\", \"warlock-the-undying-scag\");\nawait moveFile(\"classes\", \"wizard-bladesinging\", \"wizard-bladesinging-tce\");\nawait moveFile(\"classes\", \"wizard-chronurgy-magic\", \"wizard-chronurgy-magic-egw\");\nawait moveFile(\"classes\", \"wizard-graviturgy-magic\", \"wizard-graviturgy-magic-egw\");\nawait moveFile(\"classes\", \"wizard-order-of-scribes\", \"wizard-order-of-scribes-tce\");\nawait moveFile(\"classes\", \"wizard-war-magic\", \"wizard-war-magic-xge\");\n\n// Pf2e files that will be renamed if present\n\nawait moveRulesFile(\"core-rulebook\", \"conditions-appendix\", \"appendix-a-conditions-appendix\");\n\nif (count == 0) {\n    tR += \"|  |  |\\n\\nNothing to rename\\n\";\n}\ntR += \"\\n\"\n%>\n"
  },
  {
    "path": "migration/ttrpg-cli-renameFiles-5e-2.1.0.md",
    "content": "<%*\n// Templater script to rename files in the vault before updating to 2.1.0 CLI output.\n// This will move files from 1.x or 2.0.x to 2.1.x\n\n// 1. Copy this file into your templates directory\n// 2. Update the following settings:\n//\n// 2a. EDIT Path to the compendium (contains backgrounds, bestiary, etc) from the vault root\nconst compendium = \"compendium\";\n//\n// 2b. EDIT Path to the rules (contains actions, and rule variants) from the vault root\nconst rules = \"rules\";\n//\n// 2c. EDIT How many files to rename at a time?\n//     This operation can take a bit (as obsidian updates associated links),\n//     so it is best done in batches.\nconst limit = 50;\n//\n// 3. Create a new/temporary note, and use the \"Templater: Open Insert Template Modal\"\n//    and choose this template. The note content will be updated with the files that have \n//    been renamed. You can also monitor progress in Developer Tools console \n//    (View -> Toggle Developer Tools)\n\nvar f;\nlet count = 0;\n\ntR += \"| File | New Name |\\n\";\ntR += \"|------|----------|\\n\";\n\nconst c = slashify(compendium);\nconst r = slashify(rules);\n\nfunction slashify(path) {\n    if (path.length > 0 && path.endsWith(\"/\")) {\n        return path;\n    }\n    return `${path}/`;\n}\n\nasync function moveCompendiumFile(path, oldname, newname) {\n    const p = slashify(path);\n    await moveFile(`${c}${p}${oldname}`, `${c}${p}${newname}`);\n}\nasync function moveRulesFile(path, oldname, newname) {\n    const p = slashify(path);\n    await moveFile(`${r}${p}${oldname}`, `${r}${p}${newname}`);\n}\nasync function moveCompendiumToRules(oldname, newname) {\n    await moveFile(`${c}${oldname}`, `${r}${newname}`);\n}\nasync function moveRulesToCompendium(oldname, newname) {\n    await moveFile(`${r}${oldname}`, `${c}${newname}`);\n}\n\nasync function moveFile(oldname, newname) {\n    if (count > limit) {\n        return;\n    }\n    const file = await window.app.metadataCache.getFirstLinkpathDest(oldname, \"\");\n    if (file) {\n        console.log(`Moving ${oldname} to ${newname}`)\n        await this.app.fileManager.renameFile(file, newname);\n        tR += `| ${oldname} | ${newname} |\\n`;\n        count++; // increment counter after moving something\n    }\n}\n\n// 1.x to 2.0.x\n\nawait moveCompendiumFile(\"classes\", \n        \"barbarian-path-of-the-ancestral-guardian\", \n        \"barbarian-path-of-the-ancestral-guardian-xge\");\nawait moveCompendiumFile(\"classes\", \n        \"barbarian-path-of-the-battlerager\", \n        \"barbarian-path-of-the-battlerager-scag\");\nawait moveCompendiumFile(\"classes\", \n        \"barbarian-path-of-the-beast\", \n        \"barbarian-path-of-the-beast-tce\");\nawait moveCompendiumFile(\"classes\", \n        \"barbarian-path-of-the-storm-herald\", \n        \"barbarian-path-of-the-storm-herald-xge\");\nawait moveCompendiumFile(\"classes\",\n        \"barbarian-path-of-the-zealot\", \n        \"barbarian-path-of-the-zealot-xge\");\nawait moveCompendiumFile(\"classes\",\n        \"barbarian-path-of-wild-magic\", \n        \"barbarian-path-of-wild-magic-tce\");\nawait moveCompendiumFile(\"classes\",\n        \"bard-college-of-creation\", \n        \"bard-college-of-creation-tce\");\nawait moveCompendiumFile(\"classes\",\n        \"bard-college-of-eloquence\", \n        \"bard-college-of-eloquence-tce\");\nawait moveCompendiumFile(\"classes\",\n        \"bard-college-of-glamour\", \n        \"bard-college-of-glamour-xge\");\nawait moveCompendiumFile(\"classes\",\n        \"bard-college-of-swords\", \n        \"bard-college-of-swords-xge\");\nawait moveCompendiumFile(\"classes\",\n        \"bard-college-of-whispers\", \n        \"bard-college-of-whispers-xge\");\nawait moveCompendiumFile(\"classes\",\n        \"cleric-arcana-domain\", \n        \"cleric-arcana-domain-scag\");\nawait moveCompendiumFile(\"classes\",\n        \"cleric-forge-domain\", \n        \"cleric-forge-domain-xge\");\nawait moveCompendiumFile(\"classes\",\n        \"cleric-grave-domain\", \n        \"cleric-grave-domain-xge\");\nawait moveCompendiumFile(\"classes\",\n        \"cleric-order-domain\", \n        \"cleric-order-domain-tce\");\nawait moveCompendiumFile(\"classes\",\n        \"cleric-peace-domain\", \n        \"cleric-peace-domain-tce\");\nawait moveCompendiumFile(\"classes\",\n        \"cleric-twilight-domain\", \n        \"cleric-twilight-domain-tce\");\nawait moveCompendiumFile(\"classes\",\n        \"druid-circle-of-dreams\", \n        \"druid-circle-of-dreams-xge\");\nawait moveCompendiumFile(\"classes\",\n        \"druid-circle-of-spores\", \n        \"druid-circle-of-spores-tce\");\nawait moveCompendiumFile(\"classes\",\n        \"druid-circle-of-stars\", \n        \"druid-circle-of-stars-tce\");\nawait moveCompendiumFile(\"classes\",\n        \"druid-circle-of-the-shepherd\", \n        \"druid-circle-of-the-shepherd-xge\");\nawait moveCompendiumFile(\"classes\",\n        \"druid-circle-of-wildfire\", \n        \"druid-circle-of-wildfire-tce\");\nawait moveCompendiumFile(\"classes\",\n        \"fighter-arcane-archer\", \n        \"fighter-arcane-archer-xge\");\nawait moveCompendiumFile(\"classes\",\n        \"fighter-cavalier\", \n        \"fighter-cavalier-xge\");\nawait moveCompendiumFile(\"classes\",\n        \"fighter-echo-knight\", \n        \"fighter-echo-knight-egw\");\nawait moveCompendiumFile(\"classes\",\n        \"fighter-psi-warrior\", \n        \"fighter-psi-warrior-tce\");\nawait moveCompendiumFile(\"classes\",\n        \"fighter-purple-dragon-knight-banneret\", \n        \"fighter-purple-dragon-knight-banneret-scag\");\nawait moveCompendiumFile(\"classes\",\n        \"fighter-rune-knight\", \n        \"fighter-rune-knight-tce\");\nawait moveCompendiumFile(\"classes\",\n        \"fighter-samurai\", \n        \"fighter-samurai-xge\");\nawait moveCompendiumFile(\"classes\",\n        \"monk-way-of-mercy\", \n        \"monk-way-of-mercy-tce\");\nawait moveCompendiumFile(\"classes\",\n        \"monk-way-of-the-ascendant-dragon\", \n        \"monk-way-of-the-ascendant-dragon-ftd\");\nawait moveCompendiumFile(\"classes\",\n        \"monk-way-of-the-astral-self\", \n        \"monk-way-of-the-astral-self-tce\");\nawait moveCompendiumFile(\"classes\",\n        \"monk-way-of-the-drunken-master\", \n        \"monk-way-of-the-drunken-master-xge\");\nawait moveCompendiumFile(\"classes\",\n        \"monk-way-of-the-kensei\", \n        \"monk-way-of-the-kensei-xge\");\nawait moveCompendiumFile(\"classes\",\n        \"monk-way-of-the-long-death\", \n        \"monk-way-of-the-long-death-scag\");\nawait moveCompendiumFile(\"classes\",\n        \"monk-way-of-the-sun-soul\", \n        \"monk-way-of-the-sun-soul-xge\");\nawait moveCompendiumFile(\"classes\",\n        \"paladin-oath-of-conquest\", \n        \"paladin-oath-of-conquest-xge\");\nawait moveCompendiumFile(\"classes\",\n        \"paladin-oath-of-glory\", \n        \"paladin-oath-of-glory-tce\");\nawait moveCompendiumFile(\"classes\",\n        \"paladin-oath-of-redemption\", \n        \"paladin-oath-of-redemption-xge\");\nawait moveCompendiumFile(\"classes\",\n        \"paladin-oath-of-the-crown\", \n        \"paladin-oath-of-the-crown-scag\");\nawait moveCompendiumFile(\"classes\",\n        \"paladin-oath-of-the-watchers\", \n        \"paladin-oath-of-the-watchers-tce\");\nawait moveCompendiumFile(\"classes\",\n        \"ranger-drakewarden\", \n        \"ranger-drakewarden-ftd\");\nawait moveCompendiumFile(\"classes\",\n        \"ranger-fey-wanderer\", \n        \"ranger-fey-wanderer-tce\");\nawait moveCompendiumFile(\"classes\",\n        \"ranger-gloom-stalker\", \n        \"ranger-gloom-stalker-xge\");\nawait moveCompendiumFile(\"classes\",\n        \"ranger-horizon-walker\", \n        \"ranger-horizon-walker-xge\");\nawait moveCompendiumFile(\"classes\",\n        \"ranger-monster-slayer\", \n        \"ranger-monster-slayer-xge\");\nawait moveCompendiumFile(\"classes\",\n        \"ranger-swarmkeeper\", \n        \"ranger-swarmkeeper-tce\");\nawait moveCompendiumFile(\"classes\",\n        \"rogue-inquisitive\", \n        \"rogue-inquisitive-xge\");\nawait moveCompendiumFile(\"classes\",\n        \"rogue-mastermind\", \n        \"rogue-mastermind-xge\");\nawait moveCompendiumFile(\"classes\",\n        \"rogue-phantom\", \n        \"rogue-phantom-tce\");\nawait moveCompendiumFile(\"classes\",\n        \"rogue-scout\", \n        \"rogue-scout-xge\");\nawait moveCompendiumFile(\"classes\",\n        \"rogue-soulknife\", \n        \"rogue-soulknife-tce\");\nawait moveCompendiumFile(\"classes\",\n        \"rogue-swashbuckler\", \n        \"rogue-swashbuckler-xge\");\nawait moveCompendiumFile(\"classes\",\n        \"sorcerer-aberrant-mind\", \n        \"sorcerer-aberrant-mind-tce\");\nawait moveCompendiumFile(\"classes\",\n        \"sorcerer-clockwork-soul\", \n        \"sorcerer-clockwork-soul-tce\");\nawait moveCompendiumFile(\"classes\",\n        \"sorcerer-divine-soul\", \n        \"sorcerer-divine-soul-xge\");\nawait moveCompendiumFile(\"classes\",\n        \"sorcerer-shadow-magic\", \n        \"sorcerer-shadow-magic-xge\");\nawait moveCompendiumFile(\"classes\",\n        \"sorcerer-storm-sorcery\", \n        \"sorcerer-storm-sorcery-xge\");\nawait moveCompendiumFile(\"classes\",\n        \"warlock-the-celestial\", \n        \"warlock-the-celestial-xge\");\nawait moveCompendiumFile(\"classes\",\n        \"warlock-the-fathomless\", \n        \"warlock-the-fathomless-tce\");\nawait moveCompendiumFile(\"classes\",\n        \"warlock-the-genie\", \n        \"warlock-the-genie-tce\");\nawait moveCompendiumFile(\"classes\",\n        \"warlock-the-hexblade\", \n        \"warlock-the-hexblade-xge\");\nawait moveCompendiumFile(\"classes\",\n        \"warlock-the-undying\", \n        \"warlock-the-undying-scag\");\nawait moveCompendiumFile(\"classes\",\n        \"wizard-bladesinging\", \n        \"wizard-bladesinging-tce\");\nawait moveCompendiumFile(\"classes\",\n        \"wizard-chronurgy-magic\", \n        \"wizard-chronurgy-magic-egw\");\nawait moveCompendiumFile(\"classes\",\n        \"wizard-graviturgy-magic\", \n        \"wizard-graviturgy-magic-egw\");\nawait moveCompendiumFile(\"classes\",\n        \"wizard-order-of-scribes\", \n        \"wizard-order-of-scribes-tce\");\nawait moveCompendiumFile(\"classes\",\n        \"wizard-war-magic\", \n        \"wizard-war-magic-xge\");\n\n// 2.0.x to 2.1.x\n\nawait moveCompendiumFile(\"adventures\", \n        \"essentials-kit-storm-lords-wrath/img/023-ynkgo-donnabella.png\",\n        \"essentials-kit-divine-contention/img/023-ynkgo-donnabella.png\");\nawait moveCompendiumFile(\"adventures\", \n        \"essentials-kit-storm-lords-wrath/img/024-leeom-galandro.png\",\n        \"essentials-kit-divine-contention/img/024-leeom-galandro.png\");\nawait moveCompendiumFile(\"adventures\", \n        \"essentials-kit-storm-lords-wrath/img/025-l3v6r-inverna.png\",\n        \"essentials-kit-divine-contention/img/025-l3v6r-inverna.png\");\nawait moveCompendiumFile(\"adventures\", \n        \"essentials-kit-storm-lords-wrath/img/026-3krsm-nib.png\",\n        \"essentials-kit-divine-contention/img/026-3krsm-nib.png\");\nawait moveCompendiumFile(\"adventures\", \n        \"essentials-kit-storm-lords-wrath/img/027-pu4f2-pete.png\",\n        \"essentials-kit-divine-contention/img/027-pu4f2-pete.png\");\nawait moveCompendiumFile(\"adventures\", \n        \"essentials-kit-storm-lords-wrath/img/028-gttxp-quinn.png\",\n        \"essentials-kit-divine-contention/img/028-gttxp-quinn.png\");\nawait moveCompendiumFile(\"adventures\", \n        \"essentials-kit-storm-lords-wrath/img/029-1qyge-ruby.png\",\n        \"essentials-kit-divine-contention/img/029-1qyge-ruby.png\");\nawait moveCompendiumFile(\"adventures\", \n        \"essentials-kit-storm-lords-wrath/img/030-nemem-shanjan.png\",\n        \"essentials-kit-divine-contention/img/030-nemem-shanjan.png\");\nawait moveCompendiumFile(\"adventures\", \n        \"essentials-kit-storm-lords-wrath/img/031-nrqv1-talon.png\",\n        \"essentials-kit-divine-contention/img/031-nrqv1-talon.png\");\nawait moveCompendiumFile(\"adventures\", \n        \"tales-from-the-yawning-portal-dead-in-thay/img/durnan.jpg\",\n        \"tales-from-the-yawning-portal-against-the-giants/img/durnan.jpg\");\nawait moveCompendiumFile(\"bestiary\",\n        \"fiend/img/archfiend-of-ifnir.webp\",\n        \"beast/img/archfiend-of-ifnir.webp\");\nawait moveCompendiumFile(\"bestiary\",\n        \"humanoid/img/bhaal.webp\",\n        \"beast/img/bhaal.webp\");\nawait moveCompendiumFile(\"bestiary\",\n        \"npc/img/four-armed-troll.jpg\",\n        \"giant/img/four-armed-troll.jpg\");\nawait moveCompendiumFile(\"bestiary\",\n        \"beast/img/river-serpent.webp\",\n        \"monstrosity/img/river-serpent.webp\");\nawait moveCompendiumFile(\"bestiary\",\n        \"humanoid/img/045-05-005-divas-attack.webp\",\n        \"npc/img/045-05-005-divas-attack.webp\");\nawait moveCompendiumFile(\"bestiary\",\n        \"construct/img/sacred-statue.webp\",\n        \"undead/img/sacred-statue.webp\");\nawait moveCompendiumFile(\"bestiary\",\n        \"dragon/adult-red-dracolich-tce.md\",\n        \"undead/adult-red-dracolich-tce.md\");\nawait moveCompendiumFile(\"bestiary\",\n        \"dragon/token/adult-red-dracolich.png\",\n        \"undead/token/adult-red-dracolich.png\");\nawait moveCompendiumFile(\"bestiary\",\n        \"giant/ogre-skeleton-tftyp.md\",\n        \"undead/ogre-skeleton-tftyp.md\");\nawait moveCompendiumFile(\"bestiary\",\n        \"giant/token/ogre-skeleton.png\",\n        \"undead/token/ogre-skeleton.png\");\nawait moveCompendiumFile(\"books\",\n        \"dungeon-masters-screen-spelljammer/1-.md\",\n        \"dungeon-masters-screen-spelljammer/1.md\");\nawait moveCompendiumFile(\"items\",\n        \"ball-bearings-bag-of-1-000.md\",\n        \"ball-bearings-bag-of-1000.md\");\nawait moveCompendiumFile(\"tables\",\n        \"names-lizardfolk.md\",\n        \"lizardfolk-names-general-gos.md\");\nawait moveRulesFile(\"variant-rules\",\n        \"action-points.md\",\n        \"action-points-uaeberron.md\");\nawait moveRulesFile(\"variant-rules\",\n        \"adamantine-weapons.md\",\n        \"adamantine-weapons-xge.md\");\nawait moveRulesFile(\"variant-rules\"\n        \"common-languages.md\",\n        \"common-languages-uawge.md\");\nawait moveRulesFile(\"variant-rules\",\n        \"crashing-a-ship.md\",\n        \"crashing-a-ship-uaofshipsandsea.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"crashing.md\",\n        \"crashing-aag.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"creating-magic-items.md\",\n        \"creating-magic-items-uawge.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"crew.md\",\n        \"crew-aag.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"custom-alignments.md\",\n        \"custom-alignments-uavariantrules.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"customizing-your-origin.md\",\n        \"customizing-your-origin-tce.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"downtime-activity-buying-a-magic-item.md\",\n        \"downtime-activity-buying-a-magic-item-xge.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"downtime-activity-crafting-an-item.md\",\n        \"downtime-activity-crafting-an-item-xge.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"downtime-activity-crime.md\",\n        \"downtime-activity-crime-xge.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"downtime-activity-gambling.md\",\n        \"downtime-activity-gambling-xge.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"downtime-activity-pit-fighting.md\",\n        \"downtime-activity-pit-fighting-xge.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"downtime-activity-relaxation.md\",\n        \"downtime-activity-relaxation-xge.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"downtime-activity-religious-service.md\",\n        \"downtime-activity-religious-service-xge.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"downtime-activity-research.md\",\n        \"downtime-activity-research-xge.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"downtime-activity-scribing-a-spell-scroll.md\",\n        \"downtime-activity-scribing-a-spell-scroll-xge.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"downtime-activity-selling-a-magic-item.md\",\n        \"downtime-activity-selling-a-magic-item-xge.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"downtime-activity-work.md\",\n        \"downtime-activity-work-xge.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"downtime-and-franchise-activity-explore-territory.md\",\n        \"downtime-and-franchise-activity-explore-territory-ai.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"downtime-and-franchise-activity-franchise-restructuring.md\",\n        \"downtime-and-franchise-activity-franchise-restructuring-ai.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"downtime-and-franchise-activity-headquarters-modification.md\",\n        \"downtime-and-franchise-activity-headquarters-modification-ai.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"downtime-and-franchise-activity-marketeering.md\",\n        \"downtime-and-franchise-activity-marketeering-ai.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"downtime-and-franchise-activity-philanthropic-enterprise.md\",\n        \"downtime-and-franchise-activity-philanthropic-enterprise-ai.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"downtime-and-franchise-activity-running-a-franchise.md\",\n        \"downtime-and-franchise-activity-running-a-franchise-ai.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"downtime-and-franchise-activity-schmoozing.md\",\n        \"downtime-and-franchise-activity-schmoozing-ai.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"downtime-and-franchise-activity-scrutineering.md\",\n        \"downtime-and-franchise-activity-scrutineering-ai.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"downtime-and-franchise-activity-shady-business-practice.md\",\n        \"downtime-and-franchise-activity-shady-business-practice-ai.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"downtime-and-franchise-activity-team-building.md\",\n        \"downtime-and-franchise-activity-team-building-ai.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"downtime-revisited.md\",\n        \"downtime-revisited-xge.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"emrakuls-madness.md\",\n        \"emrakuls-madness-psi.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"encounters-at-sea.md\",\n        \"encounters-at-sea-gos.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"environmental-elements.md\",\n        \"environmental-elements-uawge.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"falling.md\",\n        \"falling-xge.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"fear-and-stress.md\",\n        \"fear-and-stress-vrgr.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"greyhawk-initiative.md\",\n        \"greyhawk-initiative-uagreyhawkinitiative.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"haunted-traps.md\",\n        \"haunted-traps-vrgr.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"human-languages.md\",\n        \"human-languages-scag.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"magic-tattoos.md\",\n        \"magic-tattoos-ua2020spellsandmagictattoos.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"mass-combat.md\",\n        \"mass-combat-uamasscombat.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"mysterious-islands.md\",\n        \"mysterious-islands-gos.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"ocean-environs.md\",\n        \"ocean-environs-gos.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"officers-and-crew.md\",\n        \"officers-and-crew-gos.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"optional-class-features.md\",\n        \"optional-class-features-tce.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"owning-a-ship.md\",\n        \"owning-a-ship-uaofshipsandsea.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"players-make-all-rolls.md\",\n        \"players-make-all-rolls-uavariantrules.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"prestige-classes.md\",\n        \"prestige-classes-uaprestigeclassesrunmagic.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"random-ships.md\",\n        \"random-ships-gos.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"rune-magic.md\",\n        \"rune-magic-uaprestigeclassesrunmagic.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"shared-campaign-variant-rules.md\",\n        \"shared-campaign-variant-rules-xge.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"ship-crew.md\",\n        \"ship-crew-uaofshipsandsea.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"ship-officers.md\",\n        \"ship-officers-uaofshipsandsea.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"ship-repairs.md\",\n        \"ship-repairs-aag.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"ship-stat-blocks.md\",\n        \"ship-stat-blocks-uaofshipsandsea.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"ship-to-ship-combat.md\",\n        \"ship-to-ship-combat-aag.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"ships-in-combat.md\",\n        \"ships-in-combat-uaofshipsandsea.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"sidekicks.md\",\n        \"sidekicks-uasidekicks.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"simultaneous-effects.md\",\n        \"simultaneous-effects-xge.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"sleep.md\",\n        \"sleep-xge.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"spellcasting.md\",\n        \"spellcasting-xge.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"superior-ship-upgrades.md\",\n        \"superior-ship-upgrades-gos.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"survivors.md\",\n        \"survivors-vrgr.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"swapping-racial-languages.md\",\n        \"swapping-racial-languages-uawge.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"three-pillar-experience.md\",\n        \"three-pillar-experience-uathreepillarexperience.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"tool-proficiencies.md\",\n        \"tool-proficiencies-xge.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"travel-at-sea.md\",\n        \"travel-at-sea-uaofshipsandsea.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"two-handed-arcane-focuses.md\",\n        \"two-handed-arcane-focuses-uawge.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"tying-knots.md\",\n        \"tying-knots-xge.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"vitality.md\",\n        \"vitality-uavariantrules.md\");\nawait moveRulesFile(\"variant-rules\", \n        \"wild-shape-forms.md\",\n        \"wild-shape-forms-uadruid.md\");\n\nif (count == 0) {\n    tR += \"|  |  |\\n\\nNothing to rename\\n\";\n}\ntR += \"\\n\";\n%>\n"
  },
  {
    "path": "mvnw",
    "content": "#!/bin/sh\n# ----------------------------------------------------------------------------\n# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#    http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n# ----------------------------------------------------------------------------\n\n# ----------------------------------------------------------------------------\n# Apache Maven Wrapper startup batch script, version 3.3.4\n#\n# Optional ENV vars\n# -----------------\n#   JAVA_HOME - location of a JDK home dir, required when download maven via java source\n#   MVNW_REPOURL - repo url base for downloading maven distribution\n#   MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven\n#   MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output\n# ----------------------------------------------------------------------------\n\nset -euf\n[ \"${MVNW_VERBOSE-}\" != debug ] || set -x\n\n# OS specific support.\nnative_path() { printf %s\\\\n \"$1\"; }\ncase \"$(uname)\" in\nCYGWIN* | MINGW*)\n  [ -z \"${JAVA_HOME-}\" ] || JAVA_HOME=\"$(cygpath --unix \"$JAVA_HOME\")\"\n  native_path() { cygpath --path --windows \"$1\"; }\n  ;;\nesac\n\n# set JAVACMD and JAVACCMD\nset_java_home() {\n  # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched\n  if [ -n \"${JAVA_HOME-}\" ]; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ]; then\n      # IBM's JDK on AIX uses strange locations for the executables\n      JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n      JAVACCMD=\"$JAVA_HOME/jre/sh/javac\"\n    else\n      JAVACMD=\"$JAVA_HOME/bin/java\"\n      JAVACCMD=\"$JAVA_HOME/bin/javac\"\n\n      if [ ! -x \"$JAVACMD\" ] || [ ! -x \"$JAVACCMD\" ]; then\n        echo \"The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run.\" >&2\n        echo \"JAVA_HOME is set to \\\"$JAVA_HOME\\\", but \\\"\\$JAVA_HOME/bin/java\\\" or \\\"\\$JAVA_HOME/bin/javac\\\" does not exist.\" >&2\n        return 1\n      fi\n    fi\n  else\n    JAVACMD=\"$(\n      'set' +e\n      'unset' -f command 2>/dev/null\n      'command' -v java\n    )\" || :\n    JAVACCMD=\"$(\n      'set' +e\n      'unset' -f command 2>/dev/null\n      'command' -v javac\n    )\" || :\n\n    if [ ! -x \"${JAVACMD-}\" ] || [ ! -x \"${JAVACCMD-}\" ]; then\n      echo \"The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run.\" >&2\n      return 1\n    fi\n  fi\n}\n\n# hash string like Java String::hashCode\nhash_string() {\n  str=\"${1:-}\" h=0\n  while [ -n \"$str\" ]; do\n    char=\"${str%\"${str#?}\"}\"\n    h=$(((h * 31 + $(LC_CTYPE=C printf %d \"'$char\")) % 4294967296))\n    str=\"${str#?}\"\n  done\n  printf %x\\\\n $h\n}\n\nverbose() { :; }\n[ \"${MVNW_VERBOSE-}\" != true ] || verbose() { printf %s\\\\n \"${1-}\"; }\n\ndie() {\n  printf %s\\\\n \"$1\" >&2\n  exit 1\n}\n\ntrim() {\n  # MWRAPPER-139:\n  #   Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.\n  #   Needed for removing poorly interpreted newline sequences when running in more\n  #   exotic environments such as mingw bash on Windows.\n  printf \"%s\" \"${1}\" | tr -d '[:space:]'\n}\n\nscriptDir=\"$(dirname \"$0\")\"\nscriptName=\"$(basename \"$0\")\"\n\n# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties\nwhile IFS=\"=\" read -r key value; do\n  case \"${key-}\" in\n  distributionUrl) distributionUrl=$(trim \"${value-}\") ;;\n  distributionSha256Sum) distributionSha256Sum=$(trim \"${value-}\") ;;\n  esac\ndone <\"$scriptDir/.mvn/wrapper/maven-wrapper.properties\"\n[ -n \"${distributionUrl-}\" ] || die \"cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties\"\n\ncase \"${distributionUrl##*/}\" in\nmaven-mvnd-*bin.*)\n  MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/\n  case \"${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)\" in\n  *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;\n  :Darwin*x86_64) distributionPlatform=darwin-amd64 ;;\n  :Darwin*arm64) distributionPlatform=darwin-aarch64 ;;\n  :Linux*x86_64*) distributionPlatform=linux-amd64 ;;\n  *)\n    echo \"Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version\" >&2\n    distributionPlatform=linux-amd64\n    ;;\n  esac\n  distributionUrl=\"${distributionUrl%-bin.*}-$distributionPlatform.zip\"\n  ;;\nmaven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;\n*) MVN_CMD=\"mvn${scriptName#mvnw}\" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;\nesac\n\n# apply MVNW_REPOURL and calculate MAVEN_HOME\n# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>\n[ -z \"${MVNW_REPOURL-}\" ] || distributionUrl=\"$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*\"$_MVNW_REPO_PATTERN\"}\"\ndistributionUrlName=\"${distributionUrl##*/}\"\ndistributionUrlNameMain=\"${distributionUrlName%.*}\"\ndistributionUrlNameMain=\"${distributionUrlNameMain%-bin}\"\nMAVEN_USER_HOME=\"${MAVEN_USER_HOME:-${HOME}/.m2}\"\nMAVEN_HOME=\"${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string \"$distributionUrl\")\"\n\nexec_maven() {\n  unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :\n  exec \"$MAVEN_HOME/bin/$MVN_CMD\" \"$@\" || die \"cannot exec $MAVEN_HOME/bin/$MVN_CMD\"\n}\n\nif [ -d \"$MAVEN_HOME\" ]; then\n  verbose \"found existing MAVEN_HOME at $MAVEN_HOME\"\n  exec_maven \"$@\"\nfi\n\ncase \"${distributionUrl-}\" in\n*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;\n*) die \"distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'\" ;;\nesac\n\n# prepare tmp dir\nif TMP_DOWNLOAD_DIR=\"$(mktemp -d)\" && [ -d \"$TMP_DOWNLOAD_DIR\" ]; then\n  clean() { rm -rf -- \"$TMP_DOWNLOAD_DIR\"; }\n  trap clean HUP INT TERM EXIT\nelse\n  die \"cannot create temp dir\"\nfi\n\nmkdir -p -- \"${MAVEN_HOME%/*}\"\n\n# Download and Install Apache Maven\nverbose \"Couldn't find MAVEN_HOME, downloading and installing it ...\"\nverbose \"Downloading from: $distributionUrl\"\nverbose \"Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName\"\n\n# select .zip or .tar.gz\nif ! command -v unzip >/dev/null; then\n  distributionUrl=\"${distributionUrl%.zip}.tar.gz\"\n  distributionUrlName=\"${distributionUrl##*/}\"\nfi\n\n# verbose opt\n__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''\n[ \"${MVNW_VERBOSE-}\" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v\n\n# normalize http auth\ncase \"${MVNW_PASSWORD:+has-password}\" in\n'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;\nhas-password) [ -n \"${MVNW_USERNAME-}\" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;\nesac\n\nif [ -z \"${MVNW_USERNAME-}\" ] && command -v wget >/dev/null; then\n  verbose \"Found wget ... using wget\"\n  wget ${__MVNW_QUIET_WGET:+\"$__MVNW_QUIET_WGET\"} \"$distributionUrl\" -O \"$TMP_DOWNLOAD_DIR/$distributionUrlName\" || die \"wget: Failed to fetch $distributionUrl\"\nelif [ -z \"${MVNW_USERNAME-}\" ] && command -v curl >/dev/null; then\n  verbose \"Found curl ... using curl\"\n  curl ${__MVNW_QUIET_CURL:+\"$__MVNW_QUIET_CURL\"} -f -L -o \"$TMP_DOWNLOAD_DIR/$distributionUrlName\" \"$distributionUrl\" || die \"curl: Failed to fetch $distributionUrl\"\nelif set_java_home; then\n  verbose \"Falling back to use Java to download\"\n  javaSource=\"$TMP_DOWNLOAD_DIR/Downloader.java\"\n  targetZip=\"$TMP_DOWNLOAD_DIR/$distributionUrlName\"\n  cat >\"$javaSource\" <<-END\n\tpublic class Downloader extends java.net.Authenticator\n\t{\n\t  protected java.net.PasswordAuthentication getPasswordAuthentication()\n\t  {\n\t    return new java.net.PasswordAuthentication( System.getenv( \"MVNW_USERNAME\" ), System.getenv( \"MVNW_PASSWORD\" ).toCharArray() );\n\t  }\n\t  public static void main( String[] args ) throws Exception\n\t  {\n\t    setDefault( new Downloader() );\n\t    java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );\n\t  }\n\t}\n\tEND\n  # For Cygwin/MinGW, switch paths to Windows format before running javac and java\n  verbose \" - Compiling Downloader.java ...\"\n  \"$(native_path \"$JAVACCMD\")\" \"$(native_path \"$javaSource\")\" || die \"Failed to compile Downloader.java\"\n  verbose \" - Running Downloader.java ...\"\n  \"$(native_path \"$JAVACMD\")\" -cp \"$(native_path \"$TMP_DOWNLOAD_DIR\")\" Downloader \"$distributionUrl\" \"$(native_path \"$targetZip\")\"\nfi\n\n# If specified, validate the SHA-256 sum of the Maven distribution zip file\nif [ -n \"${distributionSha256Sum-}\" ]; then\n  distributionSha256Result=false\n  if [ \"$MVN_CMD\" = mvnd.sh ]; then\n    echo \"Checksum validation is not supported for maven-mvnd.\" >&2\n    echo \"Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties.\" >&2\n    exit 1\n  elif command -v sha256sum >/dev/null; then\n    if echo \"$distributionSha256Sum  $TMP_DOWNLOAD_DIR/$distributionUrlName\" | sha256sum -c - >/dev/null 2>&1; then\n      distributionSha256Result=true\n    fi\n  elif command -v shasum >/dev/null; then\n    if echo \"$distributionSha256Sum  $TMP_DOWNLOAD_DIR/$distributionUrlName\" | shasum -a 256 -c >/dev/null 2>&1; then\n      distributionSha256Result=true\n    fi\n  else\n    echo \"Checksum validation was requested but neither 'sha256sum' or 'shasum' are available.\" >&2\n    echo \"Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties.\" >&2\n    exit 1\n  fi\n  if [ $distributionSha256Result = false ]; then\n    echo \"Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised.\" >&2\n    echo \"If you updated your Maven version, you need to update the specified distributionSha256Sum property.\" >&2\n    exit 1\n  fi\nfi\n\n# unzip and move\nif command -v unzip >/dev/null; then\n  unzip ${__MVNW_QUIET_UNZIP:+\"$__MVNW_QUIET_UNZIP\"} \"$TMP_DOWNLOAD_DIR/$distributionUrlName\" -d \"$TMP_DOWNLOAD_DIR\" || die \"failed to unzip\"\nelse\n  tar xzf${__MVNW_QUIET_TAR:+\"$__MVNW_QUIET_TAR\"} \"$TMP_DOWNLOAD_DIR/$distributionUrlName\" -C \"$TMP_DOWNLOAD_DIR\" || die \"failed to untar\"\nfi\n\n# Find the actual extracted directory name (handles snapshots where filename != directory name)\nactualDistributionDir=\"\"\n\n# First try the expected directory name (for regular distributions)\nif [ -d \"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain\" ]; then\n  if [ -f \"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD\" ]; then\n    actualDistributionDir=\"$distributionUrlNameMain\"\n  fi\nfi\n\n# If not found, search for any directory with the Maven executable (for snapshots)\nif [ -z \"$actualDistributionDir\" ]; then\n  # enable globbing to iterate over items\n  set +f\n  for dir in \"$TMP_DOWNLOAD_DIR\"/*; do\n    if [ -d \"$dir\" ]; then\n      if [ -f \"$dir/bin/$MVN_CMD\" ]; then\n        actualDistributionDir=\"$(basename \"$dir\")\"\n        break\n      fi\n    fi\n  done\n  set -f\nfi\n\nif [ -z \"$actualDistributionDir\" ]; then\n  verbose \"Contents of $TMP_DOWNLOAD_DIR:\"\n  verbose \"$(ls -la \"$TMP_DOWNLOAD_DIR\")\"\n  die \"Could not find Maven distribution directory in extracted archive\"\nfi\n\nverbose \"Found extracted Maven distribution directory: $actualDistributionDir\"\nprintf %s\\\\n \"$distributionUrl\" >\"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url\"\nmv -- \"$TMP_DOWNLOAD_DIR/$actualDistributionDir\" \"$MAVEN_HOME\" || [ -d \"$MAVEN_HOME\" ] || die \"fail to move MAVEN_HOME\"\n\nclean || :\nexec_maven \"$@\"\n"
  },
  {
    "path": "mvnw.cmd",
    "content": "<# : batch portion\r\n@REM ----------------------------------------------------------------------------\r\n@REM Licensed to the Apache Software Foundation (ASF) under one\r\n@REM or more contributor license agreements.  See the NOTICE file\r\n@REM distributed with this work for additional information\r\n@REM regarding copyright ownership.  The ASF licenses this file\r\n@REM to you under the Apache License, Version 2.0 (the\r\n@REM \"License\"); you may not use this file except in compliance\r\n@REM with the License.  You may obtain a copy of the License at\r\n@REM\r\n@REM    http://www.apache.org/licenses/LICENSE-2.0\r\n@REM\r\n@REM Unless required by applicable law or agreed to in writing,\r\n@REM software distributed under the License is distributed on an\r\n@REM \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\r\n@REM KIND, either express or implied.  See the License for the\r\n@REM specific language governing permissions and limitations\r\n@REM under the License.\r\n@REM ----------------------------------------------------------------------------\r\n\r\n@REM ----------------------------------------------------------------------------\r\n@REM Apache Maven Wrapper startup batch script, version 3.3.4\r\n@REM\r\n@REM Optional ENV vars\r\n@REM   MVNW_REPOURL - repo url base for downloading maven distribution\r\n@REM   MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven\r\n@REM   MVNW_VERBOSE - true: enable verbose log; others: silence the output\r\n@REM ----------------------------------------------------------------------------\r\n\r\n@IF \"%__MVNW_ARG0_NAME__%\"==\"\" (SET __MVNW_ARG0_NAME__=%~nx0)\r\n@SET __MVNW_CMD__=\r\n@SET __MVNW_ERROR__=\r\n@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%\r\n@SET PSModulePath=\r\n@FOR /F \"usebackq tokens=1* delims==\" %%A IN (`powershell -noprofile \"& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}\"`) DO @(\r\n  IF \"%%A\"==\"MVN_CMD\" (set __MVNW_CMD__=%%B) ELSE IF \"%%B\"==\"\" (echo %%A) ELSE (echo %%A=%%B)\r\n)\r\n@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%\r\n@SET __MVNW_PSMODULEP_SAVE=\r\n@SET __MVNW_ARG0_NAME__=\r\n@SET MVNW_USERNAME=\r\n@SET MVNW_PASSWORD=\r\n@IF NOT \"%__MVNW_CMD__%\"==\"\" (\"%__MVNW_CMD__%\" %*)\r\n@echo Cannot start maven from wrapper >&2 && exit /b 1\r\n@GOTO :EOF\r\n: end batch / begin powershell #>\r\n\r\n$ErrorActionPreference = \"Stop\"\r\nif ($env:MVNW_VERBOSE -eq \"true\") {\r\n  $VerbosePreference = \"Continue\"\r\n}\r\n\r\n# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties\r\n$distributionUrl = (Get-Content -Raw \"$scriptDir/.mvn/wrapper/maven-wrapper.properties\" | ConvertFrom-StringData).distributionUrl\r\nif (!$distributionUrl) {\r\n  Write-Error \"cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties\"\r\n}\r\n\r\nswitch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {\r\n  \"maven-mvnd-*\" {\r\n    $USE_MVND = $true\r\n    $distributionUrl = $distributionUrl -replace '-bin\\.[^.]*$',\"-windows-amd64.zip\"\r\n    $MVN_CMD = \"mvnd.cmd\"\r\n    break\r\n  }\r\n  default {\r\n    $USE_MVND = $false\r\n    $MVN_CMD = $script -replace '^mvnw','mvn'\r\n    break\r\n  }\r\n}\r\n\r\n# apply MVNW_REPOURL and calculate MAVEN_HOME\r\n# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>\r\nif ($env:MVNW_REPOURL) {\r\n  $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { \"/org/apache/maven/\" } else { \"/maven/mvnd/\" }\r\n  $distributionUrl = \"$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace \"^.*$MVNW_REPO_PATTERN\",'')\"\r\n}\r\n$distributionUrlName = $distributionUrl -replace '^.*/',''\r\n$distributionUrlNameMain = $distributionUrlName -replace '\\.[^.]*$','' -replace '-bin$',''\r\n\r\n$MAVEN_M2_PATH = \"$HOME/.m2\"\r\nif ($env:MAVEN_USER_HOME) {\r\n  $MAVEN_M2_PATH = \"$env:MAVEN_USER_HOME\"\r\n}\r\n\r\nif (-not (Test-Path -Path $MAVEN_M2_PATH)) {\r\n    New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null\r\n}\r\n\r\n$MAVEN_WRAPPER_DISTS = $null\r\nif ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {\r\n  $MAVEN_WRAPPER_DISTS = \"$MAVEN_M2_PATH/wrapper/dists\"\r\n} else {\r\n  $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + \"/wrapper/dists\"\r\n}\r\n\r\n$MAVEN_HOME_PARENT = \"$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain\"\r\n$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString(\"x2\")}) -join ''\r\n$MAVEN_HOME = \"$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME\"\r\n\r\nif (Test-Path -Path \"$MAVEN_HOME\" -PathType Container) {\r\n  Write-Verbose \"found existing MAVEN_HOME at $MAVEN_HOME\"\r\n  Write-Output \"MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD\"\r\n  exit $?\r\n}\r\n\r\nif (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {\r\n  Write-Error \"distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl\"\r\n}\r\n\r\n# prepare tmp dir\r\n$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile\r\n$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path \"$TMP_DOWNLOAD_DIR_HOLDER.dir\"\r\n$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null\r\ntrap {\r\n  if ($TMP_DOWNLOAD_DIR.Exists) {\r\n    try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }\r\n    catch { Write-Warning \"Cannot remove $TMP_DOWNLOAD_DIR\" }\r\n  }\r\n}\r\n\r\nNew-Item -Itemtype Directory -Path \"$MAVEN_HOME_PARENT\" -Force | Out-Null\r\n\r\n# Download and Install Apache Maven\r\nWrite-Verbose \"Couldn't find MAVEN_HOME, downloading and installing it ...\"\r\nWrite-Verbose \"Downloading from: $distributionUrl\"\r\nWrite-Verbose \"Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName\"\r\n\r\n$webclient = New-Object System.Net.WebClient\r\nif ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {\r\n  $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)\r\n}\r\n[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\r\n$webclient.DownloadFile($distributionUrl, \"$TMP_DOWNLOAD_DIR/$distributionUrlName\") | Out-Null\r\n\r\n# If specified, validate the SHA-256 sum of the Maven distribution zip file\r\n$distributionSha256Sum = (Get-Content -Raw \"$scriptDir/.mvn/wrapper/maven-wrapper.properties\" | ConvertFrom-StringData).distributionSha256Sum\r\nif ($distributionSha256Sum) {\r\n  if ($USE_MVND) {\r\n    Write-Error \"Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties.\"\r\n  }\r\n  Import-Module $PSHOME\\Modules\\Microsoft.PowerShell.Utility -Function Get-FileHash\r\n  if ((Get-FileHash \"$TMP_DOWNLOAD_DIR/$distributionUrlName\" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {\r\n    Write-Error \"Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property.\"\r\n  }\r\n}\r\n\r\n# unzip and move\r\nExpand-Archive \"$TMP_DOWNLOAD_DIR/$distributionUrlName\" -DestinationPath \"$TMP_DOWNLOAD_DIR\" | Out-Null\r\n\r\n# Find the actual extracted directory name (handles snapshots where filename != directory name)\r\n$actualDistributionDir = \"\"\r\n\r\n# First try the expected directory name (for regular distributions)\r\n$expectedPath = Join-Path \"$TMP_DOWNLOAD_DIR\" \"$distributionUrlNameMain\"\r\n$expectedMvnPath = Join-Path \"$expectedPath\" \"bin/$MVN_CMD\"\r\nif ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {\r\n  $actualDistributionDir = $distributionUrlNameMain\r\n}\r\n\r\n# If not found, search for any directory with the Maven executable (for snapshots)\r\nif (!$actualDistributionDir) {\r\n  Get-ChildItem -Path \"$TMP_DOWNLOAD_DIR\" -Directory | ForEach-Object {\r\n    $testPath = Join-Path $_.FullName \"bin/$MVN_CMD\"\r\n    if (Test-Path -Path $testPath -PathType Leaf) {\r\n      $actualDistributionDir = $_.Name\r\n    }\r\n  }\r\n}\r\n\r\nif (!$actualDistributionDir) {\r\n  Write-Error \"Could not find Maven distribution directory in extracted archive\"\r\n}\r\n\r\nWrite-Verbose \"Found extracted Maven distribution directory: $actualDistributionDir\"\r\nRename-Item -Path \"$TMP_DOWNLOAD_DIR/$actualDistributionDir\" -NewName $MAVEN_HOME_NAME | Out-Null\r\ntry {\r\n  Move-Item -Path \"$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME\" -Destination $MAVEN_HOME_PARENT | Out-Null\r\n} catch {\r\n  if (! (Test-Path -Path \"$MAVEN_HOME\" -PathType Container)) {\r\n    Write-Error \"fail to move MAVEN_HOME\"\r\n  }\r\n} finally {\r\n  try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }\r\n  catch { Write-Warning \"Cannot remove $TMP_DOWNLOAD_DIR\" }\r\n}\r\n\r\nWrite-Output \"MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD\"\r\n"
  },
  {
    "path": "pom.xml",
    "content": "<?xml version=\"1.0\"?>\n<project xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\"\n    xmlns=\"http://maven.apache.org/POM/4.0.0\"\n    xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n    <modelVersion>4.0.0</modelVersion>\n    <groupId>dev.ebullient</groupId>\n    <artifactId>ttrpg-convert-cli</artifactId>\n    <version>${revision}</version>\n\n    <name>ttrpg-convert-cli</name>\n    <description>TTRPG convert CLI</description>\n    <inceptionYear>2022</inceptionYear>\n    <url>https://github.com/ebullient/ttrpg-convert-cli</url>\n\n    <licenses>\n        <license>\n            <name>The Apache License, Version 2.0</name>\n            <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>\n            <distribution>repo</distribution>\n        </license>\n    </licenses>\n    <scm>\n        <connection>scm:git:https://github.com/ebullient/ttrpg-convert-cli.git</connection>\n        <developerConnection>scm:git:git@github.com/ebullient/ttrpg-convert-cli.git</developerConnection>\n        <tag>${project.version}</tag>\n        <url>https://github.com/ebullient/ttrpg-convert-cli</url>\n    </scm>\n    <issueManagement>\n        <system>GitHub</system>\n        <url>https://github.com/ebullient/ttrpg-convert-cli/issues</url>\n    </issueManagement>\n    <properties>\n        <revision>399-SNAPSHOT</revision>\n        <!-- Build -->\n        <clean-plugin.version>3.5.0</clean-plugin.version>\n        <compiler-plugin.version>3.15.0</compiler-plugin.version>\n        <maven.compiler.release>17</maven.compiler.release>\n        <formatter-plugin.version>2.29.0</formatter-plugin.version>\n        <impsort-plugin.version>1.13.0</impsort-plugin.version>\n        <javadoc-plugin.version>3.12.0</javadoc-plugin.version>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>\n        <surefire-plugin.version>3.5.5</surefire-plugin.version>\n        <git-commit-id-plugin.version>9.0.2</git-commit-id-plugin.version>\n        <skipTests>false</skipTests>\n        <skipITs>true</skipITs>\n        <!-- Quarkus -->\n        <quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>\n        <quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>\n        <quarkus.platform.version>3.32.4</quarkus.platform.version>\n        <!-- Libraries -->\n        <assertj.version>3.27.7</assertj.version>\n        <evalex.version>3.6.0</evalex.version>\n        <github-slugify.version>3.0.7</github-slugify.version>\n        <icu4j.version>78.3</icu4j.version>        <!-- Optional dependency of Slugify -->\n        <jsonschema-generator.version>4.38.0</jsonschema-generator.version>\n        <hebi-sass.version>1.0.4</hebi-sass.version>\n        <quarkus.package.jar.enabled>true</quarkus.package.jar.enabled>\n        <quarkus.package.jar.type>uber-jar</quarkus.package.jar.type>\n        <native.executable.name>ttrpg-convert</native.executable.name>\n    </properties>\n    <dependencyManagement>\n        <dependencies>\n            <dependency>\n                <groupId>${quarkus.platform.group-id}</groupId>\n                <artifactId>${quarkus.platform.artifact-id}</artifactId>\n                <version>${quarkus.platform.version}</version>\n                <type>pom</type>\n                <scope>import</scope>\n            </dependency>\n        </dependencies>\n    </dependencyManagement>\n    <dependencies>\n        <dependency>\n            <groupId>com.github.slugify</groupId>\n            <artifactId>slugify</artifactId>\n            <version>${github-slugify.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.dataformat</groupId>\n            <artifactId>jackson-dataformat-yaml</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.ibm.icu</groupId>\n            <artifactId>icu4j</artifactId>\n            <version>${icu4j.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>com.ezylang</groupId>\n            <artifactId>EvalEx</artifactId>\n            <version>${evalex.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>io.quarkus</groupId>\n            <artifactId>quarkus-qute</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.quarkus</groupId>\n            <artifactId>quarkus-picocli</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.quarkus</groupId>\n            <artifactId>quarkus-arc</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.quarkus</groupId>\n            <artifactId>quarkus-jackson</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.quarkus</groupId>\n            <artifactId>quarkus-junit5</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.assertj</groupId>\n            <artifactId>assertj-core</artifactId>\n            <version>${assertj.version}</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.github.victools</groupId>\n            <artifactId>jsonschema-generator</artifactId>\n            <version>${jsonschema-generator.version}</version>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>us.hebi.sass</groupId>\n                <artifactId>sass-cli-maven-plugin</artifactId>\n                <version>${hebi-sass.version}</version>\n                <configuration>\n                    <sassVersion>${sass.version}</sassVersion>\n                    <args>                        <!-- Any argument that should be forwarded to the sass cli -->\n                        <arg>${project.basedir}/src/scss:${project.basedir}/examples/css-snippets</arg>\n                        <arg>--no-source-map</arg>\n                    </args>\n                </configuration>\n                <executions>\n                    <execution>\n                        <id>sass-exec</id>\n                        <phase>generate-resources</phase>\n                        <goals>\n                            <goal>run</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>${quarkus.platform.group-id}</groupId>\n                <artifactId>quarkus-maven-plugin</artifactId>\n                <version>${quarkus.platform.version}</version>\n                <extensions>true</extensions>\n                <executions>\n                    <execution>\n                        <goals>\n                            <goal>build</goal>\n                            <goal>generate-code</goal>\n                            <goal>generate-code-tests</goal>\n                            <goal>native-image-agent</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <artifactId>maven-clean-plugin</artifactId>\n                <version>${clean-plugin.version}</version>\n                <configuration>\n                    <filesets>\n                        <fileset>\n                            <directory>docs</directory>\n                            <followSymlinks>false</followSymlinks>\n                            <includes>\n                                <include>sourceMap*</include>\n                                <include>templates/*</include>\n                            </includes>\n                        </fileset>\n                        <fileset>\n                            <directory>examples</directory>\n                            <followSymlinks>false</followSymlinks>\n                            <includes>\n                                <include>config/config*</include>\n                                <include>css-snippets/dnd5e-*.css</include>\n                                <include>css-snippets/pf2-*.css</include>\n                            </includes>\n                        </fileset>\n                    </filesets>\n                </configuration>\n            </plugin>\n            <plugin>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <version>${compiler-plugin.version}</version>\n                <configuration>\n                    <parameters>true</parameters>\n                </configuration>\n            </plugin>\n            <plugin>\n                <artifactId>maven-surefire-plugin</artifactId>\n                <version>${surefire-plugin.version}</version>\n                <configuration>\n                    <argLine>--add-opens java.base/java.lang=ALL-UNNAMED</argLine>\n                    <skipTests>${skipTests}</skipTests>\n                    <systemPropertyVariables>\n                        <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>\n                        <maven.home>${maven.home}</maven.home>\n                    </systemPropertyVariables>\n                </configuration>\n            </plugin>\n            <plugin>\n                <artifactId>maven-failsafe-plugin</artifactId>\n                <version>${surefire-plugin.version}</version>\n                <executions>\n                    <execution>\n                        <goals>\n                            <goal>integration-test</goal>\n                            <goal>verify</goal>\n                        </goals>\n                    </execution>\n                </executions>\n                <configuration>\n                    <skipTests>${skipITs}</skipTests>\n                    <skipITs>${skipITs}</skipITs>\n                    <argLine>--add-opens java.base/java.lang=ALL-UNNAMED</argLine>\n                    <systemPropertyVariables>\n                        <native.image.path>\n                            ${project.build.directory}/${native.executable.name}</native.image.path>\n                        <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>\n                        <maven.home>${maven.home}</maven.home>\n                    </systemPropertyVariables>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>net.revelc.code.formatter</groupId>\n                <artifactId>formatter-maven-plugin</artifactId>\n                <version>${formatter-plugin.version}</version>\n                <executions>\n                    <execution>\n                        <goals>\n                            <goal>format</goal>\n                        </goals>\n                    </execution>\n                </executions>\n                <configuration>\n                    <configFile>src/ide-config/eclipse-format.xml</configFile>\n                    <skip>${skipFormat}</skip>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>net.revelc.code</groupId>\n                <artifactId>impsort-maven-plugin</artifactId>\n                <version>${impsort-plugin.version}</version>\n                <executions>\n                    <execution>\n                        <id>sort-imports</id>\n                        <goals>\n                            <goal>sort</goal>\n                        </goals>\n                    </execution>\n                </executions>\n                <configuration>\n                    <groups>java.,javax.,jakarta.,org.,com.</groups>\n                    <staticGroups>*</staticGroups>\n                    <skip>${skipFormat}</skip>\n                    <removeUnused>true</removeUnused>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-javadoc-plugin</artifactId>\n                <version>${javadoc-plugin.version}</version>\n                <configuration>\n                    <doclet>dev.ebullient.convert.io.MarkdownDoclet</doclet>\n                    <docletPath>${project.basedir}/target/classes</docletPath>\n                    <disableNoFonts>true</disableNoFonts>\n                    <additionalOptions>\n                        <additionalOption>-d</additionalOption>\n                        <additionalOption>${project.basedir}/docs/templates</additionalOption>\n                    </additionalOptions>\n                    <useStandardDocletOptions>false</useStandardDocletOptions>\n                </configuration>\n                <executions>\n                    <execution>\n                        <id>generate-javadoc</id>\n                        <phase>process-classes</phase>\n                        <goals>\n                            <goal>javadoc</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>io.github.git-commit-id</groupId>\n                <artifactId>git-commit-id-maven-plugin</artifactId>\n                <version>${git-commit-id-plugin.version}</version>\n                <executions>\n                    <execution>\n                        <id>get-the-git-infos</id>\n                        <goals>\n                            <goal>revision</goal>\n                        </goals>\n                        <phase>initialize</phase>\n                    </execution>\n                </executions>\n                <configuration>\n                    <generateGitPropertiesFile>true</generateGitPropertiesFile>\n                    <generateGitPropertiesFilename>${project.build.outputDirectory}/git.properties</generateGitPropertiesFilename>\n                    <includeOnlyProperties>\n                        <includeOnlyProperty>^git.build.(time|version)$</includeOnlyProperty>\n                        <includeOnlyProperty>^git.commit.id.(abbrev|full)$</includeOnlyProperty>\n                    </includeOnlyProperties>\n                    <commitIdGenerationMode>full</commitIdGenerationMode>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n\n    <profiles>\n        <profile>\n            <id>native</id>\n            <activation>\n                <property>\n                    <name>native</name>\n                </property>\n            </activation>\n            <properties>\n                <maven.javadoc.skip>true</maven.javadoc.skip>\n                <quarkus.native.enabled>true</quarkus.native.enabled>\n                <quarkus.package.jar.enabled>false</quarkus.package.jar.enabled>\n                <quarkus.package.add-runner-suffix>false</quarkus.package.add-runner-suffix>\n                <quarkus.package.output-name>${native.executable.name}</quarkus.package.output-name>\n                <skipTests>true</skipTests>\n                <skipITs>false</skipITs>\n            </properties>\n        </profile>\n    </profiles>\n</project>\n"
  },
  {
    "path": "src/ide-config/eclipse-format.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<profiles version=\"15\">\n<profile kind=\"CodeFormatterProfile\" name=\"ttrpg-convert-cli\" version=\"15\">\n<setting id=\"org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines\" value=\"2147483647\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation\" value=\"0\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_assignment\" value=\"0\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_binary_expression\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_compact_if\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_compact_loops\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_conditional_expression\" value=\"80\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_enum_constants\" value=\"49\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_expressions_in_for_loop_header\" value=\"0\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_method_declaration\" value=\"0\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_module_statements\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_multiple_fields\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_parameterized_type_references\" value=\"0\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_resources_in_try\" value=\"80\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_type_arguments\" value=\"0\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_type_parameters\" value=\"0\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.align_type_members_on_columns\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.align_variable_declarations_on_columns\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.align_with_spaces\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_after_imports\" value=\"1\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_after_package\" value=\"1\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_before_field\" value=\"0\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration\" value=\"0\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_before_imports\" value=\"1\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_before_member_type\" value=\"1\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_before_method\" value=\"1\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk\" value=\"1\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_before_package\" value=\"0\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_between_import_groups\" value=\"1\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations\" value=\"1\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration\" value=\"end_of_line\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration\" value=\"end_of_line\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_array_initializer\" value=\"end_of_line\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_block_in_case\" value=\"end_of_line\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_block\" value=\"end_of_line\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration\" value=\"end_of_line\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_enum_constant\" value=\"end_of_line\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration\" value=\"end_of_line\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_lambda_body\" value=\"end_of_line\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_method_declaration\" value=\"end_of_line\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_switch\" value=\"end_of_line\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_type_declaration\" value=\"end_of_line\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.align_tags_descriptions_grouped\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.align_tags_names_descriptions\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.format_block_comments\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.format_header\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.format_html\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.format_javadoc_comments\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.format_line_comments\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.format_source_code\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.indent_parameter_description\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.indent_root_tags\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.line_length\" value=\"128\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.compact_else_if\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer\" value=\"2\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.continuation_indentation\" value=\"2\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.disabling_tag\" value=\"@formatter:off\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.enabling_tag\" value=\"@formatter:on\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.indentation.size\" value=\"4\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.indent_empty_lines\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.indent_statements_compare_to_block\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.indent_statements_compare_to_body\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_enum_constant\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_after_label\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_binary_operator\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_ellipsis\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_unary_operator\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_binary_operator\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_ellipsis\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_semicolon\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_unary_operator\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.join_lines_in_comments\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.join_wrapped_lines\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.keep_annotation_declaration_on_one_line\" value=\"one_line_never\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.keep_anonymous_type_declaration_on_one_line\" value=\"one_line_never\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.keep_code_block_on_one_line\" value=\"one_line_never\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.keep_enum_constant_declaration_on_one_line\" value=\"one_line_never\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.keep_enum_declaration_on_one_line\" value=\"one_line_never\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.keep_if_then_body_block_on_one_line\" value=\"one_line_never\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.keep_lambda_body_block_on_one_line\" value=\"one_line_never\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.keep_loop_body_block_on_one_line\" value=\"one_line_never\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.keep_method_body_on_one_line\" value=\"one_line_never\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.keep_simple_do_while_body_on_same_line\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.keep_simple_for_body_on_same_line\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.keep_simple_getter_setter_on_one_line\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.keep_simple_while_body_on_same_line\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.keep_type_declaration_on_one_line\" value=\"one_line_never\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.lineSplit\" value=\"128\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body\" value=\"0\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve\" value=\"1\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation\" value=\"common_lines\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.parentheses_positions_in_catch_clause\" value=\"common_lines\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.parentheses_positions_in_enum_constant_declaration\" value=\"common_lines\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.parentheses_positions_in_for_statment\" value=\"common_lines\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.parentheses_positions_in_if_while_statement\" value=\"common_lines\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.parentheses_positions_in_lambda_declaration\" value=\"common_lines\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.parentheses_positions_in_method_delcaration\" value=\"common_lines\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.parentheses_positions_in_method_invocation\" value=\"common_lines\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.parentheses_positions_in_switch_statement\" value=\"common_lines\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.parentheses_positions_in_try_clause\" value=\"common_lines\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.tabulation.char\" value=\"space\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.tabulation.size\" value=\"4\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.use_on_off_tags\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.wrap_before_assignment_operator\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.wrap_before_binary_operator\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.wrap_before_conditional_operator\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested\" value=\"true\"/>\n</profile>\n</profiles>\n"
  },
  {
    "path": "src/ide-config/eclipse.importorder",
    "content": "#Organize Import Order\n#Wed Jan 23 12:03:29 AEDT 2019\n0=java\n1=javax\n2=jakarta\n3=org\n4=com\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/Completion.java",
    "content": "package dev.ebullient.convert;\n\nimport picocli.AutoComplete.GenerateCompletion;\nimport picocli.CommandLine;\nimport picocli.CommandLine.Command;\n\n@Command(name = \"completion\", version = \"generate-completion \"\n        + CommandLine.VERSION, mixinStandardHelpOptions = true, header = \"bash/zsh completion:  source <(${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME})\", helpCommand = true)\npublic class Completion extends GenerateCompletion {\n\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/RpgDataConvertCli.java",
    "content": "package dev.ebullient.convert;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.Callable;\n\nimport jakarta.inject.Inject;\n\nimport dev.ebullient.convert.config.CompendiumConfig;\nimport dev.ebullient.convert.config.CompendiumConfig.Configurator;\nimport dev.ebullient.convert.config.Datasource;\nimport dev.ebullient.convert.config.TemplatePaths;\nimport dev.ebullient.convert.config.TtrpgConfig;\nimport dev.ebullient.convert.io.MarkdownWriter;\nimport dev.ebullient.convert.io.Msg;\nimport dev.ebullient.convert.io.Templates;\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.tools.ToolsIndex;\nimport io.quarkus.runtime.QuarkusApplication;\nimport io.quarkus.runtime.annotations.QuarkusMain;\nimport picocli.CommandLine;\nimport picocli.CommandLine.ArgGroup;\nimport picocli.CommandLine.Command;\nimport picocli.CommandLine.ExitCode;\nimport picocli.CommandLine.IFactory;\nimport picocli.CommandLine.IParameterExceptionHandler;\nimport picocli.CommandLine.Model.CommandSpec;\nimport picocli.CommandLine.Option;\nimport picocli.CommandLine.ParameterException;\nimport picocli.CommandLine.Parameters;\nimport picocli.CommandLine.ParseResult;\nimport picocli.CommandLine.ScopeType;\nimport picocli.CommandLine.Spec;\nimport picocli.CommandLine.UnmatchedArgumentException;\n\n@SuppressWarnings(\"CanBeFinal\")\n@QuarkusMain\n@Command(name = \"ttrpg-convert\", header = \"Convert TTRPG JSON data to markdown\", subcommands = {\n        Completion.class,\n}, description = {\n        \"%n%nThis will read from a collection of individual JSON files or a directory containing JSON files and will produce Obsidian markdown documents.\",\n}, footer = {\n        \"\",\n        \"Configuration.\",\n        \"\",\n        \"Sources, templates, and other settings should be specified in a config file. This file can be in either JSON or YAML. If no config file is specified (using -c or --config), this tool will look for config.json, and then config.yaml in the current directory.\",\n        \"\",\n        \"Use the 'from' option in the config file to filter materials by source. Only include materials from sources you own. There may be a default set of materials produced when no source is specified (e.g. those in the SRD)\",\n        \"\",\n        \"Identifiers for include/exclude rules and patters are listed in the generated index file.\",\n        \"\",\n        \"Here is a brief example (JSON). See the project README.md for details.\",\n        \"\",\n        \"{\",\n        \"    \\\"sources\\\": {\",\n        \"        \\\"adventure\\\": [\",\n        \"            \\\"LMoP\\\"\",\n        \"        ],\",\n        \"        \\\"book\\\": [\",\n        \"            \\\"PHB\\\"\",\n        \"        ],\",\n        \"        \\\"reference\\\": [\",\n        \"            \\\"VGM\\\"\",\n        \"        ]\",\n        \"    },\",\n        \"    \\\"paths\\\": {\",\n        \"        \\\"rules\\\": \\\"/compendium/rules/\\\"\",\n        \"    },\",\n        \"}\",\n        \"\",\n}, mixinStandardHelpOptions = true, versionProvider = VersionProvider.class, showDefaultValues = true)\npublic class RpgDataConvertCli implements Callable<Integer>, QuarkusApplication {\n    static final Path DEFAULT_PATH = Path.of(\"config.json\");\n\n    List<Path> input;\n    Path output;\n\n    @Inject\n    IFactory factory;\n\n    @Inject\n    Tui tui;\n\n    @Inject\n    Templates tpl;\n\n    @Spec\n    private CommandSpec spec;\n\n    @Option(names = { \"-d\", \"--debug\" }, description = \"Enable debug output\", defaultValue = \"false\", scope = ScopeType.INHERIT)\n    boolean debug;\n\n    @Option(names = { \"-v\", \"--verbose\" }, description = \"Verbose output\", defaultValue = \"false\", scope = ScopeType.INHERIT)\n    boolean verbose;\n\n    @Option(names = { \"-l\",\n            \"--log\" }, description = \"Capture output in a log file\", defaultValue = \"false\", scope = ScopeType.INHERIT)\n    boolean log;\n\n    Datasource game;\n\n    @Option(names = { \"-g\",\n            \"--game\" }, description = \"Game data source.%n  Candidates: ${COMPLETION-CANDIDATES}\", defaultValue = \"5e\", completionCandidates = Datasource.DatasourceCandidates.class)\n    void setDatasource(String datasource) {\n        try {\n            game = Datasource.matchDatasource(datasource);\n        } catch (IllegalStateException e) {\n            tui.errorf(\"Unknown game data: %s\", datasource);\n        }\n    }\n\n    @Option(names = { \"-c\", \"--config\" }, description = \"Config file\")\n    Path configPath;\n\n    @Option(names = \"--index\", description = \"Create index of keys that can be used to exclude entries\")\n    boolean writeIndex;\n\n    @ArgGroup(exclusive = false)\n    TemplatePaths templatePaths = new TemplatePaths();\n\n    @Option(names = \"-o\", description = \"Output directory\", required = true)\n    void setOutputPath(File outputDir) {\n        output = outputDir.toPath().toAbsolutePath().normalize();\n        if (output.toFile().exists() && output.toFile().isFile()) {\n            throw new ParameterException(spec.commandLine(),\n                    \"Specified output path exists and is a file: \" + output.toString());\n        }\n    }\n\n    @Parameters(description = \"Source file(s)\")\n    void setInput(List<File> inputFile) {\n        input = new ArrayList<>(inputFile.size());\n        for (File f : inputFile) {\n            input.add(f.toPath().toAbsolutePath().normalize());\n        }\n    }\n\n    @Override\n    public Integer call() {\n        if (input == null || input.isEmpty()) {\n            throw new CommandLine.MissingParameterException(spec.commandLine(), spec.args(),\n                    \"Must specify an input file\");\n        }\n        if (!output.toFile().exists() && !output.toFile().mkdirs()) {\n            tui.errorf(\"Unable to create output directory: %s\", output);\n            return ExitCode.USAGE;\n        }\n\n        boolean allOk = true;\n        tui.setTemplates(tpl);\n        tui.setOutputPath(output);\n\n        TtrpgConfig.init(tui, game);\n        Configurator configurator = new Configurator(tui);\n\n        configurator.setTemplatePaths(templatePaths);\n\n        if (configPath != null) {\n            if (configPath.toFile().exists()) {\n                // Read configuration\n                allOk = configurator.readConfiguration(configPath);\n                if (writeIndex) {\n                    tui.tryCopyFile(configPath, output.resolve(configPath.getFileName()));\n                }\n            } else {\n                tui.errorf(\"Specified config file does not exist: %s\", configPath);\n                allOk = false;\n            }\n        }\n\n        if (!allOk) {\n            return ExitCode.USAGE;\n        }\n\n        CompendiumConfig config = TtrpgConfig.getConfig();\n\n        tui.printlnf(Msg.OK, \"Finished reading config.\");\n\n        ToolsIndex index = ToolsIndex.createIndex();\n        Path toolsPath = null;\n\n        // Read provided input files\n        // Note: could test for selected game system and read paths differently\n        // ATM, both 5e and pf2e use the same general structure.\n        // Marker files are in configData\n        for (Path inputPath : input) {\n            tui.progressf(\"Reading %s\", inputPath);\n            Path input = inputPath.toAbsolutePath();\n            if (input.toFile().isDirectory()) {\n                boolean isTools = tui.readToolsDir(input, index::importTree);\n                if (isTools) { // we found the tools directory\n                    toolsPath = input;\n                } else {\n                    // this is some other directory full of json\n                    allOk &= tui.readDirectory(\"\", input, index::importTree);\n                }\n            } else {\n                allOk &= tui.readFile(input, TtrpgConfig.getFixes(inputPath.toString()), index::importTree);\n            }\n        }\n\n        // We've read all user specified files and user config.\n        if (toolsPath == null) {\n            tui.errorf(\"❌ No tools directory found. Please specify the directory containing the data files.\");\n            return ExitCode.USAGE;\n        }\n\n        // Include extra books, adventures, and homebrew from config\n        if (allOk && toolsPath != null) {\n            allOk = index.resolveSources(toolsPath);\n        }\n\n        if (!allOk) {\n            tui.warnf(\"\"\"\n                    Unable to find or read data. Check the following:\n                    - Are you specifying the right game (-g 5e OR -g pf2e)?\n                    - Check error messages to see what files couldn't be read\n                    \"\"\");\n            return ExitCode.USAGE;\n        }\n        tui.printlnf(Msg.OK, \"Finished reading data.\");\n\n        try {\n            index.prepare();\n\n            if (writeIndex) {\n                try {\n                    index.writeFullIndex(output.resolve(\"all-index.json\"));\n                    index.writeFilteredIndex(output.resolve(\"src-index.json\"));\n                } catch (IOException e) {\n                    tui.errorf(e, \"Exception: %s\", e);\n                    allOk = false;\n                }\n            }\n\n            tui.infof(Msg.WRITING, \"Writing files to %s\", output);\n            tpl.setCustomTemplates(config);\n\n            MarkdownWriter writer = new MarkdownWriter(output, tpl, tui);\n            index.markdownConverter(writer)\n                    .writeAll()\n                    .writeImages();\n\n            tui.printlnf(Msg.ALLDONE, \"All done!\");\n        } catch (Throwable e) {\n            String message = e.getMessage();\n            if (message == null) {\n                message = e.getClass().getSimpleName();\n            }\n            tui.errorf(e, \"An error occurred: %s.%n%nRun again with --log to capture details.\", message);\n            allOk = false;\n        }\n\n        return allOk ? ExitCode.OK : ExitCode.SOFTWARE;\n    }\n\n    private int executionStrategy(ParseResult parseResult) {\n        try {\n            tui.init(spec, debug, verbose, log);\n            return new CommandLine.RunLast().execute(parseResult);\n        } finally {\n            tui.close();\n        }\n    }\n\n    @Override\n    public int run(String... args) {\n        return new CommandLine(this, factory)\n                .setCaseInsensitiveEnumValuesAllowed(true)\n                .setExecutionStrategy(this::executionStrategy)\n                .setParameterExceptionHandler(new ShortErrorMessageHandler())\n                .setOut(Tui.streamToWriter(System.out))\n                .setErr(Tui.streamToWriter(System.err))\n                .execute(args);\n    }\n\n    class ShortErrorMessageHandler implements IParameterExceptionHandler {\n        public int handleParseException(ParameterException ex, String[] args) {\n            CommandLine cmd = ex.getCommandLine();\n            CommandSpec spec = cmd.getCommandSpec();\n            tui.init(spec, debug, verbose);\n\n            tui.errorf(ex, ex.getMessage());\n            UnmatchedArgumentException.printSuggestions(ex, cmd.getErr());\n\n            cmd.getErr().println(cmd.getHelp().fullSynopsis()); // normal text to error stream\n\n            if (spec.equals(spec.root())) {\n                cmd.getErr().println(cmd.getHelp().commandList()); // normal text to error stream\n            }\n            cmd.getErr().printf(\"See '%s --help' for more information.%n\", spec.qualifiedName());\n            cmd.getErr().flush();\n\n            return cmd.getExitCodeExceptionMapper() != null\n                    ? cmd.getExitCodeExceptionMapper().getExitCode(ex)\n                    : spec.exitCodeOnInvalidInput();\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/StringUtil.java",
    "content": "package dev.ebullient.convert;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.function.BiConsumer;\nimport java.util.function.BiFunction;\nimport java.util.function.BinaryOperator;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collector;\n\n/**\n * Holds common, generic string utiltity methods.\n *\n * <p>\n * This should only contain string utility methods which don't involve any domain-specific manipulation or knowledge.\n * Those should instead go in a {@link dev.ebullient.convert.tools.JsonTextConverter} or a\n * {@link dev.ebullient.convert.tools.JsonNodeReader}.\n * </p>\n */\npublic class StringUtil {\n    final static Set<String> lowercaseWords = Set.of(\n            \"a\", \"an\", \"the\", \"at\", \"by\", \"for\", \"in\", \"of\", \"on\", \"to\", \"with\",\n            \"but\", \"nor\", \"so\", \"yet\", \"and\", \"or\");\n\n    final static Set<String> conjunctionArticle = Set.of(\n            \"a\", \"an\", \"the\", \"and\", \"or\");\n\n    /**\n     * Return {@code formatString} formatted with {@code o} as the first parameter.\n     * If {@code o} is null, then return an empty string.\n     */\n    public static String format(String formatString, Object val) {\n        return val == null || (val instanceof String && ((String) val).isBlank()) ? \"\" : formatString.formatted(val);\n    }\n\n    public static String valueOrDefault(String value, String fallback) {\n        return value == null || value.isEmpty() ? fallback : value.trim();\n    }\n\n    public static String valueOrDefault(String[] parts, int index, String fallback) {\n        return index < 0 || index >= parts.length\n                ? fallback\n                : valueOrDefault(parts[index], fallback);\n    }\n\n    public static String quotedEscaped(String name) {\n        var cleaned = name;\n        // Strip leading and trailing quotes if both present\n        if (cleaned.startsWith(\"\\\"\") && cleaned.endsWith(\"\\\"\") && cleaned.length() > 1) {\n            cleaned = cleaned.substring(1, cleaned.length() - 1);\n        }\n        // Escape any remaining internal quotes for YAML\n        cleaned = cleaned.replace(\"\\\"\", \"\\\\\\\"\");\n        // Wrap in quotes\n        return \"\\\"%s\\\"\".formatted(cleaned);\n    }\n\n    public static String uppercaseFirst(String value) {\n        if (value == null || value.isEmpty()) {\n            return value;\n        }\n        Pattern pattern = Pattern.compile(\"^(\\\\P{L}*)(.*)\");\n        Matcher matcher = pattern.matcher(value);\n\n        if (matcher.matches()) {\n            String prefix = matcher.group(1); // Leading non-letters\n            String rest = matcher.group(2); // Everything else\n\n            if (rest.isEmpty()) {\n                return value; // No letters found\n            }\n\n            return prefix + Character.toUpperCase(rest.charAt(0)) + rest.substring(1);\n        }\n\n        return value;\n    }\n\n    public static boolean equal(Object o1, Object o2) {\n        return o1 == null ? o2 == null : o1.equals(o2);\n    }\n\n    public static int intOrDefault(String value, int defaultValue) {\n        try {\n            return Integer.parseInt(value);\n        } catch (NumberFormatException e) {\n            return defaultValue;\n        }\n    }\n\n    public static int intOrDefault(String[] parts, int index, int defaultValue) {\n        if (index < 0 || index >= parts.length) {\n            return defaultValue;\n        }\n        return intOrDefault(parts[index], defaultValue);\n    }\n\n    public static String asModifier(double value) {\n        return (value >= 0 ? \"+\" : \"\") + value;\n    }\n\n    public static String asModifier(int value) {\n        return (value >= 0 ? \"+\" : \"\") + value;\n    }\n\n    /**\n     * {@link #join(String, Collection)} but with the ability to accept varargs.\n     *\n     * @see #join(String, Collection)\n     */\n    public static String join(String joiner, Object o1, Object... rest) {\n        List<Object> args = new ArrayList<>();\n        args.add(o1);\n        args.addAll(Arrays.asList(rest));\n        return join(joiner, args);\n    }\n\n    /**\n     * Join the list into a single trimmed string, delimited using the given delimiter. Returns an empty string if the\n     * input list is null or empty, and ignores empty and null input elements.\n     *\n     * @param joiner The delimiter to use to join the strings\n     * @param list The input strings to join together\n     */\n    public static String join(String joiner, Collection<?> list) {\n        return list == null ? \"\" : list.stream().collect(joiningNonEmpty(joiner)).trim();\n    }\n\n    /**\n     * Like {@link #join(String, Collection)} but acts on a number of collections by first flattening them into a single\n     * collection.\n     */\n    public static String flatJoin(String joiner, Collection<?>... lists) {\n        return join(joiner, Arrays.stream(lists).flatMap(Collection::stream).toList());\n    }\n\n    /**\n     * Like {@link #joinWithPrefix(String, String, Collection)} but accept vararg inputs. This is mostly to get around\n     * being unable to pass null values to {@code List.of}.\n     *\n     * @see #joinWithPrefix(String, String, Collection)\n     * @see #join(String, Object, Object...)\n     */\n    public static String joinWithPrefix(String joiner, String prefix, Object o1, Object... rest) {\n        List<Object> args = new ArrayList<>();\n        args.add(o1);\n        args.addAll(Arrays.asList(rest));\n        return joinWithPrefix(joiner, prefix, args);\n    }\n\n    /**\n     * Like {@link #join(String, Collection)} but add a prefix to the resulting string if it's non-empty.\n     *\n     * @see #join(String, Collection)\n     */\n    public static String joinWithPrefix(String joiner, String prefix, Collection<?> list) {\n        String s = join(joiner, list);\n        if (s.isEmpty()) {\n            return \"\";\n        }\n        return isPresent(prefix) ? prefix + s : s;\n    }\n\n    /**\n     * {@link #joinConjunct(String, String, List)} with a {@code \", \"} joiner.\n     *\n     * @see #joinConjunct(List, String, String, boolean)\n     * @see #joinConjunct(String, String, List)\n     */\n    public static String joinConjunct(String lastJoiner, List<String> list) {\n        return joinConjunct(\", \", lastJoiner, list);\n    }\n\n    /**\n     * {@link #joinConjunct(List, String, String, boolean)} with a false {@code nonOxford}.\n     *\n     * @see #joinConjunct(List, String, String, boolean)\n     * @see #joinConjunct(String, List)\n     */\n    public static String joinConjunct(String joiner, String lastJoiner, List<String> list) {\n        return joinConjunct(list, joiner, lastJoiner, false);\n    }\n\n    /**\n     * Return the given list joined into a single string, using a different delimiter for the last element.\n     *\n     * <pre>\n     *     joinConjunct(List.of(\"one\", \"two\", \"three\"), \", \", \" and \", false)  // \"one, two, and three\"\n     *     joinConjunct(List.of(\"one\", \"two\", \"three\"), \", \", \" and \", true)   // \"one, two and three\"\n     * </pre>\n     *\n     * @param list The list of strings to join\n     * @param joiner The delimiter to use for all elements except the last\n     * @param lastJoiner The delimiter to add before the last element (or instead of the last delimiter, if {@code nonOxford} is\n     *        true).\n     * @param nonOxford If this is true, then don't add a normal delimiter before the final delimiter.\n     */\n    public static String joinConjunct(List<String> list, String joiner, String lastJoiner, boolean nonOxford) {\n        if (list == null || list.isEmpty()) {\n            return \"\";\n        }\n        if (list.size() == 1) {\n            return list.get(0);\n        }\n        if (list.size() == 2) {\n            return String.join(lastJoiner, list);\n        }\n\n        int pause = list.size() - 2;\n        StringBuilder out = new StringBuilder();\n        for (int i = 0; i < list.size(); ++i) {\n            out.append(list.get(i));\n\n            if (i < pause) {\n                out.append(joiner);\n            } else if (i == pause) {\n                if (!nonOxford) {\n                    out.append(joiner.trim());\n                }\n                out.append(lastJoiner);\n            }\n        }\n        return out.toString();\n    }\n\n    /** Return the given text converted to title case, with the first letter of each word capitalized. */\n    public static String toTitleCase(String text) {\n        return toTitleCase(text, false);\n    }\n\n    /**\n     * Return the given text converted to title case, with the first letter of each word capitalized.\n     *\n     * @param text The text to convert to title case\n     * @param midClause If true, then don't capitalize conjunctions and articles in the middle of a clause.\n     */\n    public static String toTitleCase(String text, boolean midClause) {\n        if (text == null || text.isEmpty()) {\n            return text;\n        }\n        String[] words = text.split(\" \", -1);\n        for (int i = 0; i < words.length; i++) {\n            String word = words[i];\n            if (word.isEmpty() || word.matches(\"\\\\P{L}+\")) {\n                continue; // Skip empty words\n            }\n            var lowerWord = word.toLowerCase();\n\n            if (midClause && conjunctionArticle.contains(lowerWord)) {\n                // Don't capitalize conjunctions\n                words[i] = lowerWord;\n            } else if (i == 0 || i == words.length - 1 || !lowercaseWords.contains(lowerWord)) {\n                // // Capitalize; Handle markdown link display text: [word] -> [Word]\n                // if (word.startsWith(\"[\") && word.length() > 1) {\n                //     words[i] = \"[\" + Character.toTitleCase(word.charAt(1)) + word.substring(2).toLowerCase();\n                // } else {\n                //     words[i] = Character.toTitleCase(word.charAt(0)) + word.substring(1).toLowerCase();\n                // }\n                words[i] = uppercaseFirst(word);\n            } else {\n                words[i] = lowerWord;\n            }\n        }\n        return String.join(\" \", words);\n    }\n\n    /** Returns true if the given string is non-null and non-blank. */\n    public static boolean isPresent(String s) {\n        return s != null && !s.isBlank();\n    }\n\n    /**\n     * Return the given string pluralized or singularized based on the input number. Use {@code assumeSingular} to\n     * correctly format non-plural inputs which end in 's' (e.g. \"walrus\").\n     *\n     * <p>\n     * <b>Known Limitations: </b>This does not handle possessives because English is difficult.\n     * </p>\n     *\n     * <p>\n     * Examples:\n     * </p>\n     *\n     * <pre>\n     *     pluralize(\"foot\", 2)    // -> \"feet\"\n     *     pluralize(\"feet\", 1)    // -> \"foot\"\n     *     pluralize(\"mile\", 2)    // -> \"miles\"\n     *     pluralize(\"miles\", 1)   // -> \"mile\"\n     *     // -> \"walrus\" (INCORRECT)\n     *     pluralize(\"walrus\", 2, false)\n     *     // -> \"walruses\" (CORRECT)\n     *     pluralize(\"walrus\", 2, true)\n     * </pre>\n     */\n    public static String pluralize(String s, Integer howMany, boolean assumeSingular) {\n        if (s == null || howMany == null) {\n            return null;\n        }\n        if (s.isEmpty()) {\n            return \"\";\n        }\n        if (s.endsWith(\"s\")) {\n            if (assumeSingular) {\n                return s + \"es\";\n            }\n            s = s.substring(0, s.length() - 1);\n        }\n        return switch (s) {\n            case \"foot\", \"feet\" -> howMany == 1 ? \"foot\" : \"feet\";\n            default -> howMany == 1 ? s : s + \"s\";\n        };\n    }\n\n    /**\n     * {@link #pluralize(String, Integer, boolean)} with {@code assumeSingular} set to {@code false}\n     *\n     * @see #pluralize(String, Integer, boolean)\n     */\n    public static String pluralize(String s, Integer howMany) {\n        return pluralize(s, howMany, false);\n    }\n\n    /**\n     * {@link #pluralize(String, String, boolean)} with {@code assumeSingular} set to {@code false}\n     *\n     * @see #pluralize(String, Integer, boolean)\n     */\n    public static String pluralize(String s, String howMany) {\n        if (!isPresent(howMany)) {\n            return s;\n        }\n        Integer howManyInt;\n        try {\n            howManyInt = Integer.parseInt(howMany);\n            return pluralize(s, howManyInt, false);\n        } catch (NumberFormatException e) {\n            return s;\n        }\n    }\n\n    /**\n     * Return the given string surrounded in parentheses. Return null if the input is null, or an empty string if\n     * the input is empty.\n     */\n    public static String parenthesize(String s) {\n        if (s == null) {\n            return null;\n        }\n        if (s.isBlank()) {\n            return \"\";\n        }\n        return \"(%s)\".formatted(s);\n    }\n\n    /** Return the given map as a formatted list of strings, formatted by the given formatter function. */\n    public static <T, U> List<String> formatMap(Map<T, U> map, BiFunction<T, U, String> formatter) {\n        return map.entrySet().stream().map(e -> formatter.apply(e.getKey(), e.getValue())).toList();\n    }\n\n    /**\n     * Returns a collector which performs like {@link #join(String, Collection)}, but usable as a collector for a\n     * stream.\n     */\n    public static <T> JoiningNonEmptyCollector<T> joiningNonEmpty(String delimiter) {\n        return joiningNonEmpty(delimiter, null);\n    }\n\n    /**\n     * Returns a collector which performs like {@link #join(String, Collection)}, but usable as a collector for a\n     * stream, with an optional prefix. Examples:\n     *\n     * <pre>\n     *     // \"Label: one, two\"\n     *     Stream.of(\"one\", \"two\").collect(joiningNonEmpty(\", \", \"Label: \"))\n     *     // \"\"\n     *     Stream.of().collect(joiningNonEmpty(\", \", \"Label: \"))\n     * </pre>\n     */\n    public static <T> JoiningNonEmptyCollector<T> joiningNonEmpty(String delimiter, String prefix) {\n        return new JoiningNonEmptyCollector<>(delimiter, null, true, prefix);\n    }\n\n    /**\n     * {@link #joiningConjunct(String, String)} with a {@code \", \"} delimiter.\n     *\n     * @see #joiningConjunct(String, String)\n     */\n    public static <T> JoiningNonEmptyCollector<T> joiningConjunct(String finalDelimiter) {\n        return joiningConjunct(finalDelimiter, \", \");\n    }\n\n    /**\n     * Returns a collector which performs like {@link #joinConjunct(String, String, List)}, but usable as a collector\n     * for a stream.\n     *\n     * <p>\n     * Example:\n     * </p>\n     *\n     * <pre>\n     * // \"one, two, and three\"\n     * Stream.of(\"one\", \"two\", \"three\").collect(joiningConjunct(\" and \", \", \"))\n     * </pre>\n     */\n    public static <T> JoiningNonEmptyCollector<T> joiningConjunct(String finalDelimiter, String delimiter) {\n        return new JoiningNonEmptyCollector<>(delimiter, finalDelimiter, true, null);\n    }\n\n    /**\n     * Returns the given number as its textual English representation. Examples:\n     *\n     * <pre>\n     *     numberAsWords(0) // -> \"zero\"\n     *     numberAsWords(3) // -> \"three\"\n     *     numberAsWords(77) // -> \"seventy-seven\"\n     *     numberAsWords(101) // -> \"101\"\n     * </pre>\n     *\n     * @param number The number to convert. For numbers greater than 100, just give the integer as a string.\n     */\n    public static String numberAsWords(int number) {\n        int abs = Math.abs(number);\n        return (number < 0 ? \"negative \" : \"\") + switch (abs) {\n            case 0 -> \"zero\";\n            case 1 -> \"one\";\n            case 2 -> \"two\";\n            case 3 -> \"three\";\n            case 4 -> \"four\";\n            case 5 -> \"five\";\n            case 6 -> \"six\";\n            case 7 -> \"seven\";\n            case 8 -> \"eight\";\n            case 9 -> \"nine\";\n            case 10 -> \"ten\";\n            case 11 -> \"eleven\";\n            case 12 -> \"twelve\";\n            case 13 -> \"thirteen\";\n            case 14 -> \"fourteen\";\n            case 15 -> \"fifteen\";\n            case 16 -> \"sixteen\";\n            case 17 -> \"seventeen\";\n            case 18 -> \"eighteen\";\n            case 19 -> \"nineteen\";\n            case 20 -> \"twenty\";\n            case 30 -> \"thirty\";\n            case 40 -> \"forty\";\n            case 50 -> \"fifty\";\n            case 60 -> \"sixty\";\n            case 70 -> \"seventy\";\n            case 80 -> \"eighty\";\n            case 90 -> \"ninety\";\n            default -> {\n                if (abs >= 100) {\n                    yield abs + \"\";\n                }\n                int r = abs % 10;\n                yield numberAsWords(abs - r) + \"-\" + numberAsWords(r);\n            }\n        };\n    }\n\n    /** Return the given {@code n} as an ordinal, e.g. 1st, 2nd, 3rd. */\n    public static String toOrdinal(Integer n) {\n        return n == null ? null : switch (n) {\n            case 1 -> \"1st\";\n            case 2 -> \"2nd\";\n            case 3 -> \"3rd\";\n            default -> n + \"th\";\n        };\n    }\n\n    /** @see #toOrdinal(Integer) */\n    public static String toOrdinal(String level) {\n        try {\n            return toOrdinal(Integer.parseInt(level));\n        } catch (NumberFormatException e) {\n            return level + \"th\";\n        }\n    }\n\n    public static String toAnchorTag(String x) {\n        return x.replace(\" \", \"%20\")\n                .replace(\":\", \"\")\n                .replace(\".\", \"\")\n                .replace('‑', '-');\n    }\n\n    // markdown link to href\n    public static String markdownLinkToHtml(String x) {\n        return x.replaceAll(\"(?<!\\\\^)\\\\[([^\\\\]]+)\\\\]\\\\(([^\\\\s)]+)(?:\\\\s\\\"[^\\\"]*\\\")?\\\\)\",\n                \"<a href=\\\"$2\\\">$1</a>\");\n    }\n\n    /**\n     * A {@link java.util.stream.Collector} which converts the elements to strings, and joins the non-empty, non-null\n     * strings into a single string. Allows providing an optional final delimiter that will be inserted before the\n     * last element.\n     *\n     * @param delimiter The delimiter used to join the strings.\n     * @param finalDelimiter The delimiter to use before the last element. Helpful for building strings like e.g.\n     *        \"one, two, and three\".\n     * @param oxford If false, then don't add a delimiter for the 'oxford comma' - e.g. replace the final delimiter\n     *        with {@code finalDelimiter} rather than adding it.\n     * @param prefix A prefix to add to the final result, if it's non-empty.\n     */\n    public record JoiningNonEmptyCollector<T>(\n            String delimiter, String finalDelimiter, Boolean oxford,\n            String prefix) implements Collector<T, List<String>, String> {\n        @Override\n        public Supplier<List<String>> supplier() {\n            return ArrayList::new;\n        }\n\n        @Override\n        public BiConsumer<List<String>, T> accumulator() {\n            return (acc, cur) -> {\n                if (cur != null && !cur.toString().isBlank()) {\n                    acc.add(cur.toString());\n                }\n            };\n        }\n\n        @Override\n        public BinaryOperator<List<String>> combiner() {\n            return (left, right) -> {\n                left.addAll(right);\n                return left;\n            };\n        }\n\n        @Override\n        public Function<List<String>, String> finisher() {\n            return acc -> {\n                String joined = isPresent(finalDelimiter)\n                        ? joinConjunct(acc, delimiter, finalDelimiter, !oxford)\n                        : String.join(delimiter, acc);\n                return isPresent(joined) && isPresent(prefix) ? prefix + joined : joined;\n            };\n        }\n\n        @Override\n        public Set<Characteristics> characteristics() {\n            return Set.of();\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/VersionProvider.java",
    "content": "package dev.ebullient.convert;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.Properties;\n\nimport dev.ebullient.convert.config.TtrpgConfig;\nimport picocli.CommandLine;\n\npublic class VersionProvider implements CommandLine.IVersionProvider {\n\n    @Override\n    public String[] getVersion() {\n        Properties properties = new Properties();\n        try (InputStream in = TtrpgConfig.class.getResourceAsStream(\"/git.properties\")) {\n            properties.load(in);\n            return new String[] {\n                    \"${COMMAND-FULL-NAME} version \" + properties.getProperty(\"git.build.version\"),\n                    \"Git commit: \" + properties.get(\"git.commit.id.abbrev\")\n            };\n        } catch (IOException e) {\n            return new String[] { \"${COMMAND-FULL-NAME} version unknown \" };\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/config/CompendiumConfig.java",
    "content": "package dev.ebullient.convert.config;\n\nimport static dev.ebullient.convert.StringUtil.isPresent;\n\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.function.BiConsumer;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.config.TtrpgConfig.Fix;\nimport dev.ebullient.convert.config.UserConfig.ImageOptions;\nimport dev.ebullient.convert.config.UserConfig.VaultPaths;\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.tools.CompendiumSources;\nimport dev.ebullient.convert.tools.ParseState;\nimport io.quarkus.runtime.annotations.RegisterForReflection;\n\n@RegisterForReflection\npublic class CompendiumConfig {\n\n    public enum DiceRoller {\n        disabled,\n        disabledUsingFS,\n        enabled,\n        enabledUsingFS;\n\n        public boolean enabled() {\n            return this == enabled || this == enabledUsingFS;\n        }\n\n        public boolean useFantasyStatblocks() {\n            return this == enabledUsingFS || this == disabledUsingFS;\n        }\n\n        public boolean useDiceRolls(ParseState state) {\n            return switch (this) {\n                case disabledUsingFS, disabled -> false;\n                case enabled -> true;\n                case enabledUsingFS -> !state.inTrait();\n            };\n        }\n\n        public boolean decorate(ParseState state) {\n            return switch (this) {\n                case enabled -> false;\n                case disabled -> true;\n                case enabledUsingFS, disabledUsingFS -> !state.inTrait();\n            };\n        }\n\n        static DiceRoller fromAttributes(Boolean useDiceRoller, Boolean yamlStatblocks) {\n            yamlStatblocks = yamlStatblocks == null ? false : yamlStatblocks;\n\n            if (useDiceRoller == null || !useDiceRoller) {\n                return yamlStatblocks ? disabledUsingFS : disabled;\n            }\n            return yamlStatblocks ? enabledUsingFS : enabled;\n        }\n    }\n\n    final static Path CWD = Path.of(\".\");\n\n    @JsonIgnore\n    final Tui tui;\n\n    Datasource datasource;\n\n    @JsonIgnore\n    final ParseState parseState = new ParseState();\n\n    String tagPrefix = \"\";\n    PathAttributes paths;\n    ImageOptions images;\n    boolean allSources = false;\n    DiceRoller useDiceRoller = DiceRoller.disabled;\n    ReprintBehavior reprintBehavior = ReprintBehavior.newest;\n    boolean racesAsSpecies = false;\n    boolean splitRules = false;\n    boolean onlyReferencedTables = false;\n    final Set<String> allowedSources = new HashSet<>();\n    final Set<String> includedKeys = new HashSet<>();\n    final Set<String> includedGroups = new HashSet<>();\n    final Set<String> excludedKeys = new HashSet<>();\n    final Set<Pattern> excludedPatterns = new HashSet<>();\n    final Set<String> homebrew = new HashSet<>();\n    final Set<String> adventures = new HashSet<>();\n    final Set<String> books = new HashSet<>();\n    final Map<String, String> defaultSource = new HashMap<>();\n    final Map<String, Path> customTemplates = new HashMap<>();\n    final Map<String, String> sourceIdAlias = new HashMap<>();\n\n    CompendiumConfig(Datasource datasource, Tui tui) {\n        this.datasource = datasource;\n        this.tui = tui;\n    }\n\n    public ParseState parseState() {\n        return parseState;\n    }\n\n    public Tui tui() {\n        return tui;\n    }\n\n    public Datasource datasource() {\n        return datasource;\n    }\n\n    public DiceRoller useDiceRoller() {\n        return useDiceRoller;\n    }\n\n    public ReprintBehavior reprintBehavior() {\n        return reprintBehavior;\n    }\n\n    public boolean racesAsSpecies() {\n        return racesAsSpecies;\n    }\n\n    public boolean splitRules() {\n        return splitRules;\n    }\n\n    public boolean onlyReferencedTables() {\n        return onlyReferencedTables;\n    }\n\n    public boolean allSources() {\n        return allSources;\n    }\n\n    public boolean noSources() {\n        return allowedSources.isEmpty();\n    }\n\n    public boolean onlySources(List<String> sources) {\n        return allowedSources.stream().allMatch(sources::contains);\n    }\n\n    public boolean readSource(Path p, List<Fix> fixes, BiConsumer<String, JsonNode> callback) {\n        return tui.readFile(p, fixes, callback);\n    }\n\n    public boolean sourceIncluded(String source) {\n        if (allSources) {\n            return true;\n        }\n        if (source == null || source.isEmpty()) {\n            return false;\n        }\n        return allowedSources.contains(source.toLowerCase());\n    }\n\n    public boolean sourcesIncluded(List<String> sources) {\n        if (allSources) {\n            return true;\n        }\n        if (sources == null || sources.isEmpty()) {\n            return false;\n        }\n        return sources.stream().anyMatch(x -> allowedSources.contains(x.toLowerCase()));\n    }\n\n    public boolean sourceIncluded(CompendiumSources source) {\n        if (allSources) {\n            return true;\n        }\n        return source.includedBy(allowedSources);\n    }\n\n    public Optional<Boolean> keyIsIncluded(String key) {\n        if (includedKeys.contains(key)) {\n            return Optional.of(true);\n        }\n        if (excludedKeys.contains(key) ||\n                excludedPatterns.stream().anyMatch(x -> x.matcher(key).matches())) {\n            return Optional.of(false);\n        }\n        return Optional.empty();\n    }\n\n    public boolean groupIsIncluded(String group) {\n        return includedGroups.contains(group);\n    }\n\n    public String rulesVaultRoot() {\n        return pathAttributes().rulesVaultRoot;\n    }\n\n    public String compendiumVaultRoot() {\n        return pathAttributes().compendiumVaultRoot;\n    }\n\n    public Path rulesFilePath() {\n        return pathAttributes().rulesFilePath;\n    }\n\n    public Path compendiumFilePath() {\n        return pathAttributes().compendiumFilePath;\n    }\n\n    public String tagOf(String... tag) {\n        return tagPrefix + Arrays.stream(tag)\n                .map(Tui::slugify)\n                .collect(Collectors.joining(\"/\"));\n    }\n\n    public String tagOfRaw(String tag) {\n        return tagPrefix + tag;\n    }\n\n    public List<String> resolveBooks() {\n        books.removeIf(x -> !isPresent(x));\n        return books.stream()\n                .map(b -> {\n                    if (b.endsWith(\".json\")) {\n                        return b;\n                    }\n                    String bl = b.toLowerCase();\n                    allowSource(bl);\n                    String id = sourceIdAlias.getOrDefault(bl, bl);\n                    allowSource(id);\n                    return \"book/book-\" + id + \".json\";\n                })\n                .toList();\n    }\n\n    public List<String> resolveAdventures() {\n        adventures.removeIf(x -> !isPresent(x));\n        return adventures.stream()\n                .map(a -> {\n                    if (a.endsWith(\".json\")) {\n                        return a;\n                    }\n                    String al = a.toLowerCase();\n                    allowSource(al);\n                    String id = sourceIdAlias.getOrDefault(al, al);\n                    allowSource(id);\n                    return \"adventure/adventure-\" + id + \".json\";\n                })\n                .toList();\n    }\n\n    public Collection<String> resolveHomebrew() {\n        homebrew.removeIf(x -> !isPresent(x));\n        return Collections.unmodifiableCollection(homebrew);\n    }\n\n    public Path getCustomTemplate(String id) {\n        return customTemplates.get(id);\n    }\n\n    public void readConfigurationIfPresent(JsonNode node) {\n        if (userConfigPresent(node)) {\n            Configurator c = new Configurator(this);\n            c.readConfigIfPresent(node);\n        }\n    }\n\n    /** Package private: add source */\n    void allowSource(String source) {\n        if (source == null || source.isEmpty()) {\n            return;\n        }\n        String s = source.toLowerCase();\n        if (\"all\".equals(s) || \"*\".equals(s)) {\n            allSources = true;\n            allowedSources.clear();\n            allowedSources.add(\"*\");\n        } else if (!allSources) {\n            allowedSources.add(s);\n        }\n\n        if (!allSources) {\n            // If this source maps to an abbreviation, include that, too\n            // This also handles source renames (freeRules2024 -> basicRules2024)\n            String abbv = TtrpgConfig.sourceToAbbreviation(s);\n            allowedSources.add(abbv);\n        }\n    }\n\n    /** Package private: add sources */\n    void allowSources(List<String> sources) {\n        if (sources == null) {\n            return;\n        }\n        for (String s : sources) {\n            allowSource(s);\n        }\n    }\n\n    private void addExcludePattern(String value) {\n        String[] split = value.split(\"\\\\|\");\n        if (split.length > 1) {\n            for (int i = 0; i < split.length - 1; i++) {\n                if (!split[i].endsWith(\"\\\\\")) {\n                    split[i] += \"\\\\\";\n                }\n            }\n        }\n        excludedPatterns.add(Pattern.compile(String.join(\"|\", split)));\n    }\n\n    private PathAttributes pathAttributes() {\n        if (paths == null) {\n            return paths = new PathAttributes();\n        }\n        return paths;\n    }\n\n    ImageOptions imageOptions() {\n        if (images == null) {\n            return images = new ImageOptions();\n        }\n        return images;\n    }\n\n    /**\n     * Create / populate CompendiumConfig in TtrpgConfig\n     */\n    public static class Configurator {\n\n        protected final Tui tui;\n\n        public Configurator(Tui tui) {\n            this.tui = tui;\n        }\n\n        public Configurator(CompendiumConfig compendiumConfig) {\n            this(compendiumConfig.tui);\n        }\n\n        public void allowSource(String src) {\n            CompendiumConfig cfg = TtrpgConfig.getConfig();\n            cfg.allowSource(src);\n        }\n\n        public void setSourceIdAlias(String src, String id) {\n            CompendiumConfig cfg = TtrpgConfig.getConfig();\n            cfg.sourceIdAlias.put(src.toLowerCase(), id.toLowerCase());\n        }\n\n        public void setTemplatePaths(TemplatePaths templatePaths) {\n            CompendiumConfig cfg = TtrpgConfig.getConfig();\n            templatePaths.verify(tui);\n            cfg.customTemplates.putAll(templatePaths.customTemplates);\n        }\n\n        public void setUseDiceRoller(DiceRoller useDiceRoller) {\n            CompendiumConfig cfg = TtrpgConfig.getConfig();\n            cfg.useDiceRoller = useDiceRoller;\n        }\n\n        /** Parse the config file at the given path */\n        public boolean readConfiguration(Path configPath) {\n            try {\n                if (configPath != null && configPath.toFile().exists()) {\n                    JsonNode node = Tui.mapper(configPath).readTree(configPath.toFile());\n                    readConfigIfPresent(node);\n                } else {\n                    tui.errorf(\"Unknown configuration file: %s\", configPath);\n                    return false;\n                }\n            } catch (IOException e) {\n                tui.errorf(e, \"Error parsing configuration file (%s): %s\",\n                        configPath, e.getMessage());\n                return false;\n            }\n            return true;\n        }\n\n        /**\n         * Reads contents of JsonNode.\n         * Will read and process user configuration keys if they are\n         * present.\n         *\n         * @param node\n         */\n        public void readConfigIfPresent(JsonNode node) {\n            if (userConfigPresent(node)) {\n                CompendiumConfig cfg = TtrpgConfig.getConfig();\n                readConfig(cfg, node);\n            }\n        }\n\n        private void readConfig(CompendiumConfig config, JsonNode node) {\n            UserConfig input = Tui.MAPPER.convertValue(node, UserConfig.class);\n\n            if (input.useDiceRoller != null || input.yamlStatblocks != null) {\n                config.useDiceRoller = DiceRoller.fromAttributes(input.useDiceRoller, input.yamlStatblocks);\n            }\n\n            input.include.forEach(s -> config.includedKeys.add(s.toLowerCase()));\n            input.includeGroup.forEach(s -> config.includedGroups.add(s.toLowerCase()));\n            input.exclude.forEach(s -> config.excludedKeys.add(s.toLowerCase()));\n            input.excludePattern.forEach(s -> config.addExcludePattern(s.toLowerCase()));\n\n            config.allowSources(input.references()); // sources + from\n            config.books.addAll(input.sources.book);\n            config.adventures.addAll(input.sources.adventure);\n            config.homebrew.addAll(input.sources.homebrew);\n\n            // map: type to default source\n            input.sources.defaultSource.entrySet().stream()\n                    .forEach(e -> {\n                        config.defaultSource.put(e.getKey().toLowerCase(), e.getValue());\n                    });\n\n            config.images = new ImageOptions(config.images, input.images);\n            config.paths = new PathAttributes(config.paths, input.paths);\n\n            if (input.tagPrefix != null && !input.tagPrefix.isEmpty()) {\n                config.tagPrefix = input.tagPrefix;\n                if (!config.tagPrefix.endsWith(\"/\")) {\n                    config.tagPrefix += \"/\";\n                }\n            }\n\n            if (!input.template.isEmpty()) {\n                TemplatePaths tplPaths = new TemplatePaths();\n                input.template.forEach((key, value) -> tplPaths.setCustomTemplate(key, Path.of(value)));\n                tplPaths.verify(tui);\n                config.customTemplates.putAll(tplPaths.customTemplates);\n            }\n\n            config.reprintBehavior = input.reprintBehavior;\n\n            if (input.racesAsSpecies != null && input.racesAsSpecies) {\n                config.racesAsSpecies = true;\n            }\n\n            if (input.splitRules != null && input.splitRules) {\n                config.splitRules = true;\n            }\n\n            if (input.onlyReferencedTables != null && input.onlyReferencedTables) {\n                config.onlyReferencedTables = true;\n            }\n        }\n    }\n\n    private static boolean userConfigPresent(JsonNode node) {\n        return Stream.of(UserConfig.ConfigKeys.values())\n                .anyMatch((k) -> k.get(node) != null);\n    }\n\n    private static class PathAttributes {\n        String rulesVaultRoot = \"rules/\";\n        String compendiumVaultRoot = \"compendium/\";\n\n        Path rulesFilePath = Path.of(\"rules/\");\n        Path compendiumFilePath = Path.of(\"compendium/\");\n\n        PathAttributes() {\n        }\n\n        public PathAttributes(PathAttributes old, VaultPaths paths) {\n            String root;\n            if (paths.rules != null) {\n                root = toRoot(paths.rules);\n                rulesFilePath = toFilesystemRoot(root);\n                rulesVaultRoot = toVaultRoot(root);\n            } else if (old != null) {\n                rulesFilePath = old.rulesFilePath;\n                rulesVaultRoot = old.rulesVaultRoot;\n            }\n            if (paths.compendium != null) {\n                root = toRoot(paths.compendium);\n                compendiumFilePath = toFilesystemRoot(root);\n                compendiumVaultRoot = toVaultRoot(root);\n            } else if (old != null) {\n                compendiumFilePath = old.compendiumFilePath;\n                compendiumVaultRoot = old.compendiumVaultRoot;\n            }\n        }\n\n        private static String toRoot(String value) {\n            if (value == null || value.isEmpty()) {\n                return \"\";\n            }\n            return (value + '/')\n                    .replace('\\\\', '/')\n                    .replaceAll(\"/+\", \"/\");\n        }\n\n        private static Path toFilesystemRoot(String root) {\n            if (root.equals(\"/\") || root.isBlank()) {\n                return CWD;\n            }\n            return Path.of(root.startsWith(\"/\") ? root.substring(1) : root);\n        }\n\n        private static String toVaultRoot(String root) {\n            return root.replaceAll(\" \", \"%20\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/config/Datasource.java",
    "content": "package dev.ebullient.convert.config;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.stream.Stream;\n\npublic enum Datasource {\n    tools5e(\"5e\", \"5etools\"),\n    toolsPf2e(\"pf2e\", \"pf2etools\");\n\n    public final List<String> format;\n\n    Datasource(String... format) {\n        this.format = List.of(format);\n    }\n\n    public String shortName() {\n        return this.format.get(0);\n    }\n\n    public static Datasource matchDatasource(String input) {\n        String key = input.toLowerCase();\n        Optional<Datasource> value = Stream.of(Datasource.values())\n                .filter((d) -> d.name().equals(key) || d.format.contains(key))\n                .findFirst();\n        if (value.isEmpty()) {\n            throw new IllegalArgumentException(\"Unknown data source: \" + input);\n        }\n        return value.get();\n    }\n\n    public static class DatasourceCandidates extends ArrayList<String> {\n        DatasourceCandidates() {\n            super(List.of(\"5e\", \"pf2e\"));\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/config/ReprintBehavior.java",
    "content": "package dev.ebullient.convert.config;\n\npublic enum ReprintBehavior {\n    newest, // Newest only\n    edition, // Follow reprints within an edition\n    all; // Ignore reprints\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/config/TemplatePaths.java",
    "content": "package dev.ebullient.convert.config;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport dev.ebullient.convert.io.Tui;\nimport picocli.CommandLine.Option;\n\npublic class TemplatePaths {\n\n    public final Map<String, Path> customTemplates = new HashMap<>();\n    public final Map<String, Path> badTemplates = new HashMap<>();\n\n    public void setCustomTemplate(String key, Path path) {\n        key = toTemplateKey(key);\n\n        if (Files.isRegularFile(path)) {\n            customTemplates.put(key, path);\n            return;\n        }\n\n        Path resolved = Path.of(\"\").resolve(path);\n        if (Files.isRegularFile(resolved)) {\n            customTemplates.put(key, resolved);\n            return;\n        }\n        badTemplates.put(key, path);\n    }\n\n    @Option(names = { \"--background\" }, order = 1, hidden = true, description = \"Path to Qute template for Backgrounds\")\n    void setBackgroundTemplatePath(Path path) {\n        setCustomTemplate(\"background\", path);\n    }\n\n    @Option(names = { \"--class\" }, order = 2, hidden = true, description = \"Path to Qute template for Classes\")\n    void setClassTemplatePath(Path path) {\n        setCustomTemplate(\"class\", path);\n    }\n\n    @Option(names = { \"--deity\" }, order = 3, hidden = true, description = \"Path to Qute template for Deities\")\n    void setDeityTemplatePath(Path path) {\n        setCustomTemplate(\"deity\", path);\n    }\n\n    @Option(names = { \"--feat\" }, order = 4, hidden = true, description = \"Path to Qute template for Feats\")\n    void setFeatTemplatePath(Path path) {\n        setCustomTemplate(\"feat\", path);\n    }\n\n    @Option(names = { \"--item\" }, order = 5, hidden = true, description = \"Path to Qute template for Items\")\n    void setItemTemplatePath(Path path) {\n        setCustomTemplate(\"item\", path);\n    }\n\n    @Option(names = { \"--monster\" }, order = 6, hidden = true, description = \"Path to Qute template for Monsters\")\n    void setMonsterTemplatePath(Path path) {\n        setCustomTemplate(\"monster\", path);\n    }\n\n    @Option(names = { \"--name\" }, order = 7, hidden = true, description = \"Path to Qute template for Names\")\n    void setNameTemplatePath(Path path) {\n        setCustomTemplate(\"name\", path);\n    }\n\n    @Option(names = { \"--note\" }, order = 8, hidden = true, description = \"Path to Qute template for Notes\")\n    void setNoteTemplatePath(Path path) {\n        setCustomTemplate(\"note\", path);\n    }\n\n    @Option(names = { \"--race\" }, order = 9, hidden = true, description = \"Path to Qute template for Races\")\n    void setRaceTemplatePath(Path path) {\n        setCustomTemplate(\"race\", path);\n    }\n\n    @Option(names = { \"--spell\" }, order = 10, hidden = true, description = \"Path to Qute template for Spells\")\n    void setSpellTemplatePath(Path path) {\n        setCustomTemplate(\"spell\", path);\n    }\n\n    @Option(names = { \"--subclass\" }, order = 11, hidden = true, description = \"Path to Qute template for Subclasses\")\n    void setSubclassTemplatePath(Path path) {\n        setCustomTemplate(\"subclass\", path);\n    }\n\n    public Path get(String id) {\n        return customTemplates.get(id);\n    }\n\n    public void verify(Tui tui) {\n        Map<String, Path> badKeys = new HashMap<>();\n\n        // Check template keys after game system config has been loaded\n        customTemplates.forEach((k, v) -> {\n            if (!TtrpgConfig.getTemplateKeys().contains(toConfigKey(k))) {\n                badKeys.put(k, v);\n            }\n        });\n        if (badKeys.isEmpty() && badTemplates.isEmpty()) {\n            return;\n        }\n        badKeys.forEach((k, v) -> {\n            customTemplates.remove(k);\n            tui.errorf(\"Unknown template key %s. Valid keys: %s\",\n                    toConfigKey(k), TtrpgConfig.getTemplateKeys());\n        });\n        badTemplates.forEach((k, v) -> {\n            tui.errorf(\"Template file specified for '%s' (%s) does not exist or is not a file.\",\n                    toConfigKey(k), v);\n        });\n        tui.throwInvalidArgumentException(\"Bad template specified\");\n    }\n\n    private String toTemplateKey(String key) {\n        return key + (key.startsWith(\"index\") ? \".txt\" : \"2md.txt\");\n    }\n\n    private String toConfigKey(String key) {\n        return key.replace(\"2md.txt\", \"\")\n                .replace(\".txt\", \"\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/config/TtrpgConfig.java",
    "content": "package dev.ebullient.convert.config;\n\nimport java.io.File;\nimport java.nio.file.Path;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.TreeSet;\nimport java.util.function.Consumer;\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.NullNode;\n\nimport dev.ebullient.convert.config.CompendiumConfig.Configurator;\nimport dev.ebullient.convert.config.UserConfig.ImageOptions;\nimport dev.ebullient.convert.io.Msg;\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.tools.IndexType;\nimport dev.ebullient.convert.tools.JsonNodeReader;\nimport io.quarkus.runtime.annotations.RegisterForReflection;\n\npublic class TtrpgConfig {\n\n    public static final String DEFAULT_IMG_ROOT = \"imgRoot\";\n\n    static final Set<String> missingSourceName = new HashSet<>();\n\n    private static Datasource datasource;\n    private static CompendiumConfig activeConfig = null;\n    private static DatasourceConfig datasourceConfig = null;\n    private static Tui tui;\n    private static ImageRoot internalImageRoot;\n    private static Path toolsPath;\n\n    public static void init(Tui tui) {\n        init(tui, null);\n    }\n\n    public static void init(Tui tui, Datasource datasource) {\n        TtrpgConfig.tui = tui;\n        TtrpgConfig.internalImageRoot = null;\n        TtrpgConfig.toolsPath = null;\n        TtrpgConfig.activeConfig = null;\n        TtrpgConfig.datasource = datasource == null ? Datasource.tools5e : datasource;\n        TtrpgConfig.datasourceConfig = new DatasourceConfig();\n        TtrpgConfig.missingSourceName.clear();\n        readSystemConfig();\n    }\n\n    public static CompendiumConfig getConfig() {\n        if (activeConfig == null) {\n            activeConfig = new CompendiumConfig(TtrpgConfig.datasource, tui);\n        }\n        return activeConfig;\n    }\n\n    public static String getConstant(String key) {\n        return activeDSConfig().constants.get(key);\n    }\n\n    public static void setToolsPath(Path toolsPath) {\n        TtrpgConfig.toolsPath = toolsPath;\n    }\n\n    private static DatasourceConfig activeDSConfig() {\n        return datasourceConfig;\n    }\n\n    /**\n     * Change what is considered the default source for the type.\n     * This only applies to the determination of file suffixes, not the\n     * default used for key/reference resolution.\n     */\n    public static String getDefaultOutputSource(IndexType type) {\n        return getConfig().defaultSource.getOrDefault(type.name(), type.defaultSourceString());\n    }\n\n    public static String getDefaultOutputSource(String key) {\n        return getConfig().defaultSource.get(key);\n    }\n\n    public static List<Fix> getFixes(String filepath) {\n        return activeDSConfig().findFixesFor(filepath.replace('\\\\', '/'));\n    }\n\n    public static String sourceToLongName(String src) {\n        String abbreviation = sourceToAbbreviation(src).toLowerCase();\n        SourceReference ref = activeDSConfig().reference.get(abbreviation);\n        return ref == null ? src : ref.name;\n    }\n\n    public static String sourceToAbbreviation(String src) {\n        return activeDSConfig().longToAbv.getOrDefault(src.toLowerCase(), src);\n    }\n\n    public static String sourcePublicationDate(String src) {\n        String abbreviation = sourceToAbbreviation(src).toLowerCase();\n        SourceReference ref = activeDSConfig().reference.get(abbreviation);\n        return ref == null || ref.date == null ? \"1970-01-01\" : ref.date; // utils.json: ascSortDateString\n    }\n\n    public static Collection<String> getTemplateKeys() {\n        return activeDSConfig().templateKeys;\n    }\n\n    public static boolean addHomebrewSource(String name, String abv, String longAbv) {\n        return activeDSConfig().addSource(name, abv, longAbv);\n    }\n\n    public static void sourceToIdMapping(String id, String src) {\n        Configurator config = new Configurator(getConfig());\n        config.setSourceIdAlias(id, src);\n    }\n\n    public static void includeAdditionalSource(String src) {\n        CompendiumConfig config = getConfig();\n        config.allowSource(src);\n        // Books and Adventures use an id in the file name that may not\n        // match the source abbreviation. When we add a source this way,\n        // see if that mapping exists, and allow both.\n        for (Entry<String, String> entry : config.sourceIdAlias.entrySet()) {\n            if (entry.getValue().equals(src)) {\n                config.allowSource(entry.getKey());\n            }\n        }\n    }\n\n    public static void addReferenceEntries(Consumer<JsonNode> callback) {\n        if (datasource == Datasource.tools5e) {\n            JsonNode srdEntries = TtrpgConfig.activeGlobalConfig(\"srdEntries\");\n            for (JsonNode property : ConfigKeys.properties.iterateArrayFrom(srdEntries)) {\n                callback.accept(property);\n            }\n        }\n    }\n\n    public static class ImageRoot {\n        final String internalImageRoot;\n        final boolean copyInternal;\n        final boolean copyExternal;\n        final Map<String, String> fallbackPaths;\n\n        private ImageRoot(String cfgRoot, ImageOptions options) {\n            this.copyExternal = options.copyExternal();\n            this.fallbackPaths = options.fallbackPaths();\n\n            if (cfgRoot == null) {\n                this.internalImageRoot = \"\";\n                this.copyInternal = false;\n            } else {\n                if (cfgRoot.startsWith(\"http\") || cfgRoot.startsWith(\"file:\")) {\n                    this.internalImageRoot = endWithSlash(cfgRoot);\n                    this.copyInternal = options.copyInternal();\n                } else {\n                    Path imgPath = Path.of(\"\").resolve(cfgRoot).normalize().toAbsolutePath();\n                    if (!imgPath.toFile().exists()) {\n                        tui.errorf(\"Image root %s does not exist\", imgPath);\n                        this.internalImageRoot = \"\";\n                        this.copyInternal = false;\n                        return;\n                    }\n                    this.internalImageRoot = endWithSlash(imgPath.toString());\n                    this.copyInternal = options.copyInternal();\n                }\n                Tui.instance().infof(\"Using %s as the source for remote images (copyInternal=%s)\",\n                        this.internalImageRoot, this.copyInternal);\n            }\n        }\n\n        public String getRootPath() {\n            return internalImageRoot;\n        }\n\n        public String getRootPathUrl() {\n            return internalImageRoot.startsWith(\"http\") || internalImageRoot.startsWith(\"file\")\n                    ? internalImageRoot\n                    : \"file://\" + internalImageRoot;\n        }\n\n        public boolean copyInternalToVault() {\n            return copyInternal;\n        }\n\n        public boolean copyExternalToVault() {\n            return copyExternal;\n        }\n\n        public String getFallbackPath(String key) {\n            return fallbackPaths.getOrDefault(key, key);\n        }\n    }\n\n    public static ImageRoot internalImageRoot() {\n        ImageRoot root = internalImageRoot;\n        if (root == null) {\n            ImageOptions options = getConfig().imageOptions();\n            String cfg = options.internalRoot;\n            if (cfg == null) {\n                String imgRoot = activeDSConfig().constants.get(DEFAULT_IMG_ROOT);\n                if (imgRoot == null && toolsPath != null && datasource == Datasource.toolsPf2e) {\n                    cfg = toolsPath.resolve(\"..\").normalize().toString();\n                } else if (imgRoot == null) {\n                    cfg = \"\";\n                } else {\n                    cfg = imgRoot;\n                }\n            }\n            internalImageRoot = root = new ImageRoot(cfg, options);\n        }\n        return root;\n    }\n\n    private static String endWithSlash(String path) {\n        if (path == null) {\n            return \"\";\n        }\n        return path.endsWith(\"/\") ? path : path + \"/\";\n    }\n\n    public static JsonNode readIndex(String key) {\n        String file = activeDSConfig().indexes.get(key);\n        Optional<Path> root = file == null ? Optional.empty() : tui.resolvePath(Path.of(file));\n        if (root.isEmpty()) {\n            return NullNode.getInstance();\n        }\n        File indexFile = root.get().resolve(file).toFile();\n        try {\n            return Tui.MAPPER.readTree(indexFile);\n        } catch (Exception e) {\n            tui.errorf(\"Failed to read index file %s: %s\", indexFile, e.getMessage());\n            return NullNode.getInstance();\n        }\n    }\n\n    public static JsonNode activeGlobalConfig(String key) {\n        return activeDSConfig().data.get(key);\n    }\n\n    public static void checkKnown(Collection<String> bookSources) {\n        DatasourceConfig activeConfig = activeDSConfig();\n        bookSources.forEach(s -> {\n            String check = s.toLowerCase();\n            if (activeConfig.reference.containsKey(check)) {\n                return;\n            }\n            String alternate = activeConfig.longToAbv.get(check);\n            if (alternate != null) {\n                return;\n            }\n            if (missingSourceName.add(check)) {\n                tui.warnf(Msg.SOURCE, \"Source %s is unknown\", s);\n            }\n        });\n    }\n\n    public static Collection<String> getFileSources() {\n        DatasourceConfig activeConfig = activeDSConfig();\n        return Collections.unmodifiableSet(activeConfig.sourceFiles);\n    }\n\n    public static void addDefaultAliases(Map<String, String> aliases) {\n        DatasourceConfig activeConfig = activeDSConfig();\n        activeConfig.aliases.forEach((k, v) -> aliases.putIfAbsent(k, v));\n    }\n\n    private static void readSystemConfig() {\n        JsonNode node = Tui.readTreeFromResource(\"/convertData.json\");\n        readSystemConfig(node);\n\n        node = Tui.readTreeFromResource(\"/sourceMap.yaml\");\n        readSystemConfig(node);\n    }\n\n    // Global config: path mapping for missing images\n    protected static void readSystemConfig(JsonNode node) {\n        if (datasource == Datasource.tools5e) {\n            JsonNode config5e = ConfigKeys.config5e.getFrom(node);\n            if (config5e != null) {\n                JsonNode srdEntries = ConfigKeys.srdEntries.getFrom(config5e);\n                if (srdEntries != null) {\n                    datasourceConfig.data.put(ConfigKeys.srdEntries.name(), srdEntries);\n                }\n                JsonNode basicRules = ConfigKeys.basicRules.getFrom(config5e);\n                if (basicRules != null) {\n                    datasourceConfig.data.put(ConfigKeys.basicRules.name(), basicRules);\n                }\n                JsonNode basicRules2024 = ConfigKeys.basicRules2024.getFrom(config5e);\n                if (basicRules2024 != null) {\n                    datasourceConfig.data.put(ConfigKeys.basicRules2024.name(), basicRules2024);\n                }\n                readCommonSystemConfig(config5e);\n            }\n        }\n        if (datasource == Datasource.toolsPf2e) {\n            JsonNode configPf2e = ConfigKeys.configPf2e.getFrom(node);\n            if (configPf2e != null) {\n                readCommonSystemConfig(configPf2e);\n            }\n        }\n    }\n\n    protected static void readCommonSystemConfig(JsonNode source) {\n        datasourceConfig.constants.putAll(ConfigKeys.constants.getAsMap(source));\n        datasourceConfig.aliases.putAll(ConfigKeys.aliases.getAsMap(source));\n        datasourceConfig.reference.putAll(ConfigKeys.reference.getAsKeyLowerRefMap(source));\n        datasourceConfig.longToAbv.putAll(ConfigKeys.longToAbv.getAsKeyLowerMap(source));\n        datasourceConfig.fallbackImagePaths.putAll(ConfigKeys.fallbackImage.getAsMap(source));\n        datasourceConfig.sourceFiles.addAll(ConfigKeys.sourceFiles.getAsList(source));\n        datasourceConfig.indexes.putAll(ConfigKeys.indexes.getAsKeyLowerMap(source));\n        datasourceConfig.templateKeys.addAll(ConfigKeys.templateKeys.getAsList(source));\n\n        Map<String, List<Fix>> fixes = ConfigKeys.fixes.getAs(source, FIXES);\n        if (fixes != null) {\n            datasourceConfig.fixes.putAll(fixes);\n        }\n    }\n\n    static class DatasourceConfig {\n        final Map<String, JsonNode> data = new HashMap<>();\n        final Map<String, String> constants = new HashMap<>();\n        final Map<String, String> aliases = new HashMap<>();\n        final Map<String, SourceReference> reference = new HashMap<>();\n        final Map<String, String> longToAbv = new HashMap<>();\n        final Map<String, String> fallbackImagePaths = new HashMap<>();\n        final Map<String, List<Fix>> fixes = new HashMap<>();\n        final Map<String, String> indexes = new HashMap<>();\n        final Set<String> sourceFiles = new HashSet<>();\n        final Set<String> templateKeys = new TreeSet<>();\n\n        public List<Fix> findFixesFor(String filepath) {\n            for (Map.Entry<String, List<Fix>> entry : fixes.entrySet()) {\n                if (filepath.endsWith(entry.getKey())) {\n                    return entry.getValue();\n                }\n            }\n            return List.of();\n        }\n\n        public boolean addSource(String name, String abv, String longAbv) {\n            String key = abv.toLowerCase();\n            if (reference.containsKey(key)) {\n                tui.errorf(\"Duplicate source abbreviation %s for %s\", abv, name);\n                return false;\n            }\n            reference.put(key, new SourceReference(name));\n\n            if (longAbv != null) {\n                String longKey = longAbv.toLowerCase();\n                if (!key.equals(longKey)) {\n                    if (longToAbv.containsKey(longKey)) {\n                        tui.errorf(\"Duplicate source key %s for %s -> %s\", longKey, abv, name);\n                    } else {\n                        longToAbv.put(longKey, key);\n                    }\n                }\n            }\n            return true;\n        }\n    }\n\n    public final static TypeReference<Map<String, SourceReference>> MAP_REFERENCE = new TypeReference<>() {\n    };\n\n    public final static TypeReference<Map<String, List<Fix>>> FIXES = new TypeReference<>() {\n    };\n\n    @RegisterForReflection\n    static class SourceReference {\n        String name;\n        String type;\n        String date;\n\n        SourceReference() {\n        }\n\n        SourceReference(String name) {\n            this.name = name;\n        }\n    }\n\n    @RegisterForReflection\n    public static class Fix {\n        public String _comment;\n        public String match;\n        public String replace;\n    }\n\n    enum ConfigKeys implements JsonNodeReader {\n        aliases,\n        abvToName,\n        basicRules, // 5e\n        basicRules2024, // 5e\n        config5e,\n        configPf2e,\n        constants,\n        fallbackImage,\n        internalImageRoot,\n        fixes,\n        indexes,\n        longToAbv,\n        properties,\n        reference,\n        sourceFiles,\n        srdEntries,\n        templateKeys,\n        ;\n\n        public <T> T getAs(JsonNode node, TypeReference<T> ref) {\n            JsonNode obj = node.get(this.name());\n            return obj == null\n                    ? null\n                    : Tui.MAPPER.convertValue(obj, ref);\n        }\n\n        Map<String, String> getAsMap(JsonNode node) {\n            JsonNode map = node.get(this.name());\n            return map == null\n                    ? Map.of()\n                    : Tui.MAPPER.convertValue(map, Tui.MAP_STRING_STRING);\n        }\n\n        Map<String, String> getAsKeyLowerMap(JsonNode node) {\n            JsonNode map = node.get(this.name());\n            if (map == null) {\n                return Map.of();\n            }\n            Map<String, String> result = new HashMap<>();\n            for (var e : map.properties()) {\n                result.put(e.getKey().toLowerCase(), e.getValue().asText());\n            }\n            return result;\n        }\n\n        Map<String, SourceReference> getAsKeyLowerRefMap(JsonNode node) {\n            JsonNode map = node.get(this.name());\n            if (map == null) {\n                return Map.of();\n            }\n            Map<String, SourceReference> result = new HashMap<>();\n            for (var e : map.properties()) {\n                String key = e.getKey().toLowerCase();\n                SourceReference ref = Tui.MAPPER.convertValue(e.getValue(), SourceReference.class);\n                result.put(key, ref);\n            }\n            return result;\n        }\n\n        List<String> getAsList(JsonNode node) {\n            JsonNode list = node.get(this.name());\n            return list == null\n                    ? List.of()\n                    : Tui.MAPPER.convertValue(list, Tui.LIST_STRING);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/config/UserConfig.java",
    "content": "package dev.ebullient.convert.config;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Optional;\n\nimport com.fasterxml.jackson.annotation.JsonAlias;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport io.quarkus.runtime.annotations.RegisterForReflection;\n\n@RegisterForReflection\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\npublic class UserConfig {\n\n    @JsonAlias({ \"convert\", \"full-source\", \"fullSource\" })\n    Sources sources = new Sources();\n\n    @Deprecated\n    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)\n    List<String> from = new ArrayList<>();\n\n    VaultPaths paths = new VaultPaths();\n\n    List<String> include = new ArrayList<>();\n\n    List<String> includePattern = new ArrayList<>();\n\n    List<String> includeGroup = new ArrayList<>();\n\n    List<String> exclude = new ArrayList<>();\n\n    List<String> excludePattern = new ArrayList<>();\n\n    ReprintBehavior reprintBehavior = ReprintBehavior.newest;\n\n    Map<String, String> template = new HashMap<>();\n\n    Boolean useDiceRoller = null;\n    Boolean yamlStatblocks = null;\n    Boolean racesAsSpecies = null;\n    Boolean splitRules = null;\n    Boolean onlyReferencedTables = null;\n\n    String tagPrefix = \"\";\n\n    ImageOptions images = new ImageOptions();\n\n    List<String> references() {\n        List<String> reference = new ArrayList<>();\n        reference.addAll(sources.reference);\n        if (from != null) {\n            reference.addAll(from);\n        }\n        return reference;\n    }\n\n    enum ConfigKeys {\n        defaultSource,\n        exclude,\n        excludePattern,\n        fallbackPaths(List.of(\"fallback-paths\")),\n        from,\n        images,\n        include,\n        includeGroups,\n        includePattern,\n        paths,\n        reprintBehavior,\n        sources(List.of(\"fullSource\", \"full-source\", \"convert\")),\n        tagPrefix,\n        template,\n        onlyReferencedTables,\n        racesAsSpecies,\n        splitRules,\n        useDiceRoller,\n        yamlStatblocks,\n        ;\n\n        final List<String> aliases;\n\n        ConfigKeys() {\n            aliases = List.of();\n        }\n\n        ConfigKeys(List<String> aliases) {\n            this.aliases = aliases;\n        }\n\n        JsonNode get(JsonNode node) {\n            JsonNode child = node.get(this.name());\n            if (child == null) {\n                Optional<JsonNode> y = aliases.stream()\n                        .map(node::get)\n                        .filter(Objects::nonNull)\n                        .findFirst();\n                return y.orElse(null);\n            }\n            return child;\n        }\n    }\n\n    @RegisterForReflection\n    @JsonInclude(JsonInclude.Include.NON_EMPTY)\n    static class VaultPaths {\n        String compendium;\n        String rules;\n    }\n\n    @RegisterForReflection\n    @JsonInclude(JsonInclude.Include.NON_EMPTY)\n    static class Sources {\n        String toolsRoot;\n        List<String> reference = new ArrayList<>();\n        List<String> adventure = new ArrayList<>();\n        List<String> book = new ArrayList<>();\n        List<String> homebrew = new ArrayList<>();\n        Map<String, String> defaultSource = new HashMap<>();\n    }\n\n    @RegisterForReflection\n    @JsonInclude(JsonInclude.Include.NON_EMPTY)\n    static class ImageOptions {\n        String internalRoot;\n        Boolean copyInternal;\n        Boolean copyExternal;\n        final Map<String, String> fallbackPaths = new HashMap<>();\n\n        public ImageOptions() {\n        }\n\n        public ImageOptions(ImageOptions images, ImageOptions images2) {\n            if (images != null) {\n                copyExternal = images.copyExternal;\n                copyInternal = images.copyInternal;\n                internalRoot = images.internalRoot;\n                fallbackPaths.putAll(images.fallbackPaths);\n            }\n            if (images2 != null) {\n                copyExternal = images2.copyExternal == null\n                        ? copyExternal\n                        : images2.copyExternal;\n                copyInternal = images2.copyInternal == null\n                        ? copyInternal\n                        : images2.copyInternal;\n                internalRoot = images2.internalRoot == null\n                        ? internalRoot\n                        : images2.internalRoot;\n                fallbackPaths.putAll(images2.fallbackPaths);\n            }\n        }\n\n        public boolean copyExternal() {\n            return copyExternal != null && copyExternal;\n        }\n\n        public boolean copyInternal() {\n            return copyInternal != null && copyInternal;\n        }\n\n        public Map<String, String> fallbackPaths() {\n            return Collections.unmodifiableMap(fallbackPaths);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/io/FontRef.java",
    "content": "package dev.ebullient.convert.io;\n\npublic class FontRef {\n    /** Font family */\n    public final String fontFamily;\n    /** Path to font source (unresolved local or remote) */\n    public final String sourcePath;\n\n    boolean hasTextReference = false;\n\n    private FontRef(String fontFamily, String sourcePath) {\n        this.fontFamily = fontFamily;\n        this.sourcePath = sourcePath;\n    }\n\n    public void addTextReference() {\n        hasTextReference = true;\n    }\n\n    public boolean hasTextReference() {\n        return hasTextReference;\n    }\n\n    @Override\n    public String toString() {\n        return \"FontRef [fontFamily=\" + fontFamily + \", sourcePath=\" + sourcePath + \"]\";\n    }\n\n    public static String fontFamily(String fontPath) {\n        fontPath = fontPath.trim();\n        int pos1 = fontPath.lastIndexOf('/');\n        int pos2 = fontPath.lastIndexOf('.');\n        if (pos1 > 0 && pos2 > 0) {\n            fontPath = fontPath.substring(pos1 + 1, pos2);\n        } else if (pos1 > 0) {\n            fontPath = fontPath.substring(pos1 + 1);\n        } else if (pos2 > 0) {\n            fontPath = fontPath.substring(0, pos2);\n        }\n        return fontPath;\n    }\n\n    public static FontRef of(String fontString) {\n        return of(fontFamily(fontString), fontString);\n    }\n\n    public static FontRef of(String fontFamily, String fontString) {\n        if (fontString == null || fontString.isEmpty()) {\n            return null;\n        }\n        return new FontRef(fontFamily, fontString);\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/io/JavadocIgnore.java",
    "content": "package dev.ebullient.convert.io;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n@Retention(RetentionPolicy.SOURCE)\n@Target({ ElementType.METHOD, ElementType.TYPE })\npublic @interface JavadocIgnore {\n\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/io/JavadocVerbatim.java",
    "content": "package dev.ebullient.convert.io;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n@Retention(RetentionPolicy.SOURCE)\n@Target({ ElementType.FIELD, ElementType.METHOD })\npublic @interface JavadocVerbatim {\n    // include this method using the exact method or field name\n\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/io/MarkdownDoclet.java",
    "content": "package dev.ebullient.convert.io;\n\nimport java.io.IOException;\nimport java.io.PrintWriter;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.ArrayDeque;\nimport java.util.ArrayList;\nimport java.util.Deque;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.TreeMap;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\n\nimport javax.lang.model.SourceVersion;\nimport javax.lang.model.element.Element;\nimport javax.lang.model.element.ElementKind;\nimport javax.lang.model.element.Modifier;\nimport javax.lang.model.element.Name;\nimport javax.lang.model.element.NestingKind;\nimport javax.lang.model.element.PackageElement;\nimport javax.lang.model.element.QualifiedNameable;\nimport javax.lang.model.element.TypeElement;\nimport javax.lang.model.type.DeclaredType;\nimport javax.lang.model.type.TypeKind;\nimport javax.lang.model.type.TypeMirror;\nimport javax.lang.model.util.ElementFilter;\nimport javax.lang.model.util.Elements;\nimport javax.tools.Diagnostic;\nimport javax.tools.DocumentationTool;\nimport javax.tools.ToolProvider;\n\nimport com.sun.source.doctree.DocCommentTree;\nimport com.sun.source.doctree.DocTree;\nimport com.sun.source.doctree.LinkTree;\nimport com.sun.source.doctree.LiteralTree;\nimport com.sun.source.doctree.ParamTree;\nimport com.sun.source.doctree.TextTree;\nimport com.sun.source.util.DocTrees;\n\nimport jdk.javadoc.doclet.Doclet;\nimport jdk.javadoc.doclet.DocletEnvironment;\nimport jdk.javadoc.doclet.Reporter;\n\npublic class MarkdownDoclet implements Doclet {\n    Pattern preformattedText = Pattern.compile(\"```|<pre>\");\n\n    Reporter reporter;\n    DocletEnvironment environment;\n    Path outputDirectory;\n    Path currentResource;\n    Set<PackageElement> packages;\n    Map<String, String> classNameMapping = new HashMap<>();\n    /** A map of unqualified class names to their qualified class name. */\n    Map<String, String> qualifiedClassNameMapping = new HashMap<>();\n\n    MarkdownOption targetDir = new MarkdownOption() {\n        String value;\n\n        @Override\n        public int getArgumentCount() {\n            return 2;\n        }\n\n        @Override\n        public String getDescription() {\n            return \"The target output directory.\";\n        }\n\n        @Override\n        public Kind getKind() {\n            return Kind.OTHER;\n        }\n\n        @Override\n        public List<String> getNames() {\n            return List.of(\"-d\", \"--destination\");\n        }\n\n        @Override\n        public String getParameters() {\n            return \"directory\";\n        }\n\n        public String getValue() {\n            return (value != null) ? value : \"docs/templates/\";\n        }\n\n        @Override\n        public boolean process(String option, List<String> arguments) {\n            value = arguments.get(0);\n            return true;\n        }\n    };\n\n    @Override\n    public void init(Locale locale, Reporter reporter) {\n        this.reporter = reporter;\n    }\n\n    @Override\n    public String getName() {\n        return getClass().getSimpleName();\n    }\n\n    @Override\n    public SourceVersion getSupportedSourceVersion() {\n        return SourceVersion.latest();\n    }\n\n    @Override\n    public Set<? extends Option> getSupportedOptions() {\n        return Set.of(targetDir);\n    }\n\n    @Override\n    public boolean run(DocletEnvironment environment) {\n        try {\n            System.out.println(\"TTRPG Convert Cli Markdown Doclet: run begin\");\n            System.out.println(\"target: \" + targetDir.getValue());\n            processFiles(environment);\n            System.out.println(\"TTRPG Convert Cli Markdown Doclet: run end\");\n        } catch (final Exception e) {\n            reporter.print(Diagnostic.Kind.ERROR, e.getMessage());\n            e.printStackTrace();\n            return false;\n        }\n        return true;\n    }\n\n    /**\n     * Process classes and create markdown files.\n     *\n     * @throws IOException\n     */\n    protected void processFiles(DocletEnvironment environment) throws IOException {\n        this.environment = environment;\n        DocTrees docTrees = environment.getDocTrees();\n\n        outputDirectory = Paths.get(targetDir.getValue());\n        if (!Files.exists(outputDirectory)) {\n            Files.createDirectories(outputDirectory);\n        }\n        reporter.print(Diagnostic.Kind.NOTE, \"Writing to \" + outputDirectory.toAbsolutePath());\n\n        Set<? extends Element> elements = environment.getIncludedElements();\n\n        // Find TOP_LEVEL elements that enclose (interesting) others\n        ElementFilter.typesIn(elements).stream()\n                .filter(t -> isQute(t)) // only include template-related classes\n                .filter(t -> !isIgnored(t)) // skip @JavadocIgnore and Builder classes\n                .filter(t -> t.getNestingKind() != NestingKind.TOP_LEVEL) // find nested elements\n                .filter(t -> t.getKind() != ElementKind.INTERFACE) // skip inner interfaces\n                .map(TypeElement::getEnclosingElement) // map to enclosing element\n                .distinct() // remove duplicates\n                .forEach(te -> {\n                    // Append \"README\" to the class name to generate a README file\n                    // inside the directory for GH-based documentation\n                    String reference = te.toString();\n                    classNameMapping.put(reference, reference + \".README\");\n                });\n\n        // Print package indexes (README.md)\n        packages = ElementFilter.packagesIn(elements);\n        for (PackageElement p : packages) {\n            if (isQute(p) && !isIgnored(p)) {\n                writeReadmeFile(docTrees, p);\n            }\n        }\n\n        for (TypeElement t : ElementFilter.typesIn(elements)) {\n            if (!isQute(t) || isExcluded(t)) {\n                continue;\n            }\n            String mapping = classNameMapping.get(t.getQualifiedName().toString());\n            System.out.println(\n                    t.getKind().toString().substring(0, 4)\n                            + \"\\t\" + t.getQualifiedName()\n                            + (mapping == null ? \"\" : \"\\n\\t-- \" + mapping));\n            writeReferenceFile(docTrees, t);\n        }\n    }\n\n    private void debugFile(String type, Name name, Path outFile) {\n        // String out = outFile.toString().replace(targetDir.getValue(), \"\");\n        // System.out.println(type + \", \" + name.toString() + \" --> \" + out);\n    }\n\n    protected void writeReferenceFile(DocTrees docTrees, TypeElement t) throws IOException {\n        String name = t.getSimpleName().toString();\n        if (name.contains(\"Builder\")) {\n            return;\n        }\n        Path outFile = getOutputFile(t);\n        debugFile(\"reference\", t.getQualifiedName(), outFile);\n        try (PrintWriter out = new PrintWriter(Files.newBufferedWriter(outFile))) {\n            Aggregator aggregator = new Aggregator();\n            aggregator.add(\"# \" + name + \"\\n\\n\");\n\n            // Add class description\n            aggregator.addFullBody(docTrees.getDocCommentTree(t));\n\n            // Add class attributes and methods\n            Map<String, Element> members = new TreeMap<>();\n            for (Element e : t.getEnclosedElements()) {\n                processElement(docTrees, members, e);\n            }\n\n            // Include attributes and methods from superclass\n            TypeElement superType = getSuperclassElement(t);\n            while (superType != null) {\n                for (Element e : superType.getEnclosedElements()) {\n                    processElement(docTrees, members, e);\n                }\n                superType = getSuperclassElement(superType);\n            }\n\n            aggregator.add(\"\\n\\n## Attributes\\n\\n\");\n            aggregator.add(members.keySet().stream()\n                    .map(s -> String.format(\"[%s](#%s)\", s, s.toLowerCase()))\n                    .collect(Collectors.joining(\", \")));\n            aggregator.add(\"\\n\");\n\n            Map<String, List<? extends DocTree>> recordContent = new HashMap<>();\n            if (t.getKind() == ElementKind.RECORD) {\n                // If it's a record, then we can't retrieve the attributes as Elements, so we have to parse them from\n                // the comment tree instead.\n                docTrees.getDocCommentTree(t)\n                        .getBlockTags().stream()\n                        .filter(e -> e.getKind() == DocTree.Kind.PARAM)\n                        .map(param -> (ParamTree) param)\n                        .filter(p -> !p.getName().toString().startsWith(\"_\")) // fields with \"_\" prefix are internal\n                        .forEach(param -> {\n                            recordContent.put(param.getName().toString(), param.getDescription());\n                        });\n            }\n\n            for (Map.Entry<String, Element> entry : members.entrySet()) {\n                aggregator.add(\"\\n\\n### \" + entry.getKey() + \"\\n\\n\");\n                var content = recordContent.get(entry.getKey());\n                if (content != null) {\n                    aggregator.addAll(content);\n                } else {\n                    aggregator.addFullBody(docTrees.getDocCommentTree(entry.getValue()));\n                }\n            }\n\n            out.println(aggregator);\n            out.flush();\n        }\n    }\n\n    protected void processElement(DocTrees docTrees, Map<String, Element> members, Element e) {\n        String name = e.getSimpleName().toString();\n        ElementKind kind = e.getKind();\n\n        if (isIgnored(e)) {\n            // Return early if the element is annotated with @JavadocIgnore\n            return;\n        }\n\n        if (!isIncludedVerbatim(e)) {\n            // If the element is not annotated with @JavadocVerbatim,\n            // filter and format the element name\n\n            if (!e.getModifiers().stream().anyMatch(m -> m == Modifier.PUBLIC)\n                    || e.getModifiers().stream().anyMatch(m -> m == Modifier.STATIC)) {\n                // Skip non-public and static elements\n                return;\n            }\n            if (kind == ElementKind.METHOD) {\n                if (!name.startsWith(\"get\") && !name.startsWith(\"is\")) {\n                    // Skip methods that don't start with \"get\" or \"is\"\n                    return;\n                }\n                if (e.getAnnotation(Deprecated.class) != null) {\n                    // Skip deprecated methods\n                    return;\n                }\n\n                name = name.replaceFirst(\"(get|is)\", \"\");\n                name = name.substring(0, 1).toLowerCase() + name.substring(1);\n            } else if (!kind.isField() && kind != ElementKind.RECORD_COMPONENT) {\n                // Skip any other non-field elements\n                return;\n            }\n        }\n\n        members.put(name, e);\n    }\n\n    void writeReadmeFile(DocTrees docTrees, PackageElement p) throws IOException {\n        Path outFile = getOutputFile(p);\n        debugFile(\"readme\", p.getQualifiedName(), outFile);\n\n        try (PrintWriter out = new PrintWriter(Files.newBufferedWriter(outFile))) {\n            Aggregator aggregator = new Aggregator();\n\n            // Add package description\n            aggregator.addFullBody(docTrees.getDocCommentTree(p));\n\n            // Make list linking to package members\n            Map<String, TypeElement> members = new TreeMap<>();\n            for (Element e : p.getEnclosedElements()) {\n                TypeElement te = (TypeElement) e;\n                if (isExcluded(e)) {\n                    continue;\n                }\n                if (te.getKind() == ElementKind.INTERFACE) {\n                    continue;\n                }\n                members.put(te.getSimpleName().toString(), te);\n            }\n\n            Aggregator elements = new Aggregator();\n            members.values().forEach(te -> {\n                var ref = classNameMapping.get(te.getQualifiedName().toString());\n                if (ref == null) {\n                    elements.add(\"- [\" + te.getSimpleName() + \"](\" + te.getSimpleName() + \".md)\"\n                            + getDescription(docTrees, te) + \"\\n\");\n                } else {\n                    elements.add(\"- [\" + te.getSimpleName() + \"](\" + te.getSimpleName() + \"/README.md)\"\n                            + getDescription(docTrees, te) + \"\\n\");\n                }\n            });\n\n            String result = elements.toString();\n            if (!result.isEmpty()) {\n                aggregator.add(\"\\n\\n## References\\n\\n\");\n                aggregator.add(result);\n            }\n            out.println(aggregator.toString());\n            out.flush();\n        }\n    }\n\n    private boolean isQute(QualifiedNameable e) {\n        return e.getQualifiedName().toString().contains(\"qute\");\n    }\n\n    boolean isIgnored(Element element) {\n        return element.getAnnotation(JavadocIgnore.class) != null\n                || element.getSimpleName().toString().contains(\"Builder\")\n                || element.getSimpleName().toString().contains(\"DeclaringClass\");\n    }\n\n    boolean isIncludedVerbatim(Element element) {\n        return element.getAnnotation(JavadocVerbatim.class) != null;\n    }\n\n    boolean isExcluded(Element element) {\n        if (isIncludedVerbatim(element)) {\n            return false;\n        }\n\n        boolean excludeKind = switch (element.getKind()) {\n            case CLASS, INTERFACE, RECORD, ENUM -> false;\n            default -> true;\n        };\n\n        return !environment.isIncluded(element)\n                || isIgnored(element)\n                || excludeKind;\n    }\n\n    String getDescription(DocTrees docTrees, TypeElement te) {\n        DocCommentTree docCommentTree = docTrees.getDocCommentTree(te);\n        if (docCommentTree != null) {\n            Aggregator aggregator = new Aggregator();\n            aggregator.addAll(docCommentTree.getFirstSentence());\n            var value = aggregator.toString().replace(\"\\n\", \"\\n    \")\n                    .replaceAll(\"#+ \", \"\")\n                    .trim();\n            if (value.contains(\"\\n\")) {\n                value += \"\\n\";\n            }\n            return \": \" + value;\n        }\n        return \"\";\n    }\n\n    Path getOutputFile(QualifiedNameable element) throws IOException {\n        Path outFile = currentResource = outputDirectory.resolve(qualifiedNameToPath(element));\n        if (!Files.exists(outFile.getParent())) {\n            Files.createDirectories(outFile.getParent());\n        }\n        reporter.print(Diagnostic.Kind.NOTE, \"Writing \" + outFile.toAbsolutePath());\n        return outFile;\n    }\n\n    static TypeElement getSuperclassElement(TypeElement typeElement) {\n        TypeMirror superclass = typeElement.getSuperclass();\n        if (superclass.getKind().equals(TypeKind.NONE)) {\n            return null;\n        }\n        if (superclass.toString().equals(\"java.lang.Object\")) {\n            return null;\n        }\n        if (superclass.toString().equals(\"java.lang.Record\")) {\n            return null;\n        }\n        return (TypeElement) ((DeclaredType) superclass).asElement();\n    }\n\n    String qualifiedNameToPath(QualifiedNameable element) {\n        return qualifiedNameToPath(element.getQualifiedName().toString());\n    }\n\n    String qualifiedNameToPath(String reference) {\n        reference = classNameMapping.getOrDefault(reference, reference);\n        if (reference.endsWith(\"qute\")) {\n            reference += \".README\";\n        } else if (!isValidClass(reference.replace(\".README\", \"\"))) {\n            // Check if the reference is a valid class\n            throw new RuntimeException(\"Invalid class reference: \" + reference);\n        }\n        return reference\n                .replace(\"dev.ebullient.convert.\", \"\")\n                .replace(\"tools.\", \"\")\n                .replace(\"qute.\", \"\")\n                .replace(\".\", \"/\")\n                + \".md\";\n    }\n\n    static interface MarkdownOption extends Option {\n        String getValue();\n    }\n\n    class Aggregator {\n        List<String> content = new ArrayList<>();\n        Deque<HtmlElement> htmlEntity = new ArrayDeque<>();\n\n        void addFullBody(DocCommentTree docCommentTree) {\n            if (docCommentTree == null) {\n                return;\n            }\n            addAll(docCommentTree.getFullBody());\n        }\n\n        void addAll(List<? extends DocTree> docTrees) {\n            if (docTrees == null) {\n                return;\n            }\n            for (DocTree docTree : docTrees) {\n                add(docTree);\n            }\n        }\n\n        void add(DocTree docTree) {\n            switch (docTree.getKind()) {\n                case TEXT:\n                    // Always remove single leading javadoc space\n                    String text = ((TextTree) docTree).getBody().toString()\n                            .replaceAll(\"\\n \", \"\\n\")\n                            .replaceAll(\"\\n\\n\\n\", \"\\n\\n\"); // consolidate extra lines\n\n                    Matcher m = preformattedText.matcher(text);\n                    if (!m.find()) {\n                        // if there isn't any pre-formatted text, remove any other leading whitespace\n                        text = text.replaceAll(\"\\n +\", \"\\n\");\n                    }\n                    add(text);\n                    break;\n                case CODE:\n                case LITERAL:\n                    add(\"`\" + ((LiteralTree) docTree).getBody().toString() + \"`\");\n                    break;\n                case START_ELEMENT:\n                    startEntity(docTree.toString());\n                    break;\n                case END_ELEMENT:\n                    endEntity(docTree.toString());\n                    break;\n                case LINK:\n                    LinkTree linkTree = (LinkTree) docTree;\n                    String reference = linkTree.getReference().toString();\n\n                    // look at label before anchor has been removed from reference\n                    String label = linkTree.getLabel().toString();\n                    if (label == null || label.isEmpty()) {\n                        int classBegin = reference.lastIndexOf(\".\");\n                        label = classBegin > -1 ? reference.substring(classBegin + 1) : reference;\n                    }\n\n                    // remove anchor from the class reference\n                    String anchor = \"\";\n                    int hash = reference.indexOf(\"#\");\n                    if (hash > -1) {\n                        anchor = reference.substring(hash);\n                        reference = reference.substring(0, hash);\n                    }\n\n                    reference = maybeGetQualifiedName(reference);\n                    // resolve the class reference to a path (and then make that link relative)\n                    reference = qualifiedNameToPath(reference);\n                    if (!reference.startsWith(\"http\")) {\n                        Path target = outputDirectory.resolve(reference);\n                        if (target.equals(currentResource)) {\n                            reference = \"\";\n                        } else {\n                            Path relative = currentResource.getParent().relativize(target);\n                            reference = relative.toString();\n                        }\n                        anchor = anchor\n                                .replaceFirst(\"^#(get|is)\", \"#\")\n                                .replace(\"()\", \"\").toLowerCase();\n                        label = label\n                                .replaceFirst(\"#(get|is)\", \"#\")\n                                .replace(\"()\", \"\");\n                    }\n                    add(String.format(\"[%s](%s%s)\", label, reference, anchor));\n                    break;\n                case ENTITY:\n                    add(replacementFor(docTree.toString()));\n                    break;\n                default:\n                    System.out.println(\"tree kind: \" + docTree.getKind() + \", content: \" + docTree);\n                    break;\n            }\n        }\n\n        /** Try to get a qualified name for the reference. If we can't, then return the reference unchanged. */\n        private String maybeGetQualifiedName(String reference) {\n            Elements elemUtil = environment.getElementUtils();\n            if (reference.startsWith(\"dev.ebullient.convert\")) {\n                return reference;\n            }\n            // If we've already seen this before, then retrieve that name\n            if (qualifiedClassNameMapping.containsKey(reference)) {\n                return qualifiedClassNameMapping.get(reference);\n            }\n            // Try to get a unique qualified name from the reference\n            List<String> qualifiedNames = packages.stream()\n                    .map(pkg -> elemUtil.getTypeElement(pkg.getQualifiedName().toString() + \".\" + reference))\n                    .filter(Objects::nonNull)\n                    .map(e -> e.getQualifiedName().toString())\n                    .toList();\n            // If we can't get a single unique name, then end it here and just return the original reference\n            if (qualifiedNames.size() != 1) {\n                return reference;\n            }\n            qualifiedClassNameMapping.put(reference, qualifiedNames.get(0));\n            return qualifiedNames.get(0);\n        }\n\n        void add(String text) {\n            if (htmlEntity.isEmpty()) {\n                content.add(text);\n            } else {\n                htmlEntity.peek().add(text);\n            }\n        }\n\n        void startEntity(String text) {\n            HtmlElement element = new HtmlElement(text);\n            if (!htmlEntity.isEmpty()) {\n                element.testOuter(htmlEntity.peek());\n            }\n            htmlEntity.push(element);\n        }\n\n        void endEntity(String text) {\n            HtmlElement element = htmlEntity.pop();\n            element.end(text);\n            add(element.sanitize());\n        }\n\n        @Override\n        public String toString() {\n            while (!htmlEntity.isEmpty()) {\n                endEntity(\"\");\n            }\n            return String.join(\"\", content)\n                    .replaceAll(\"(?m) +$\", \"\")\n                    .replaceAll(\"</br/>\", \"\")\n                    .replace(\"\\n\\n\\n\", \"\\n\\n\")\n                    .replaceAll(\"<br ?/?>\", \"  \\n\") // do this late.\n                    .trim();\n        }\n    }\n\n    static class HtmlElement {\n        List<String> html = new ArrayList<>();\n        String li = \"\";\n        String indent = \"\";\n        String tag;\n\n        public HtmlElement(String text) {\n            html.add(text);\n            tag = text.replaceAll(\"<([^ >]+).*\", \"$1\");\n            if (tag.equalsIgnoreCase(\"ol\")) {\n                li = \"1. \";\n            } else if (tag.equalsIgnoreCase(\"ul\")) {\n                li = \"- \";\n            }\n        }\n\n        public void testOuter(HtmlElement peek) {\n            boolean listBegin = tag.equalsIgnoreCase(\"ol\") || tag.equalsIgnoreCase(\"ul\");\n            if (listBegin && peek != null && !peek.indent.isEmpty()) {\n                indent = peek.indent + \"  \";\n            }\n            if (tag.equals(\"li\")) {\n                li = peek == null ? \"- \" : peek.li;\n            }\n        }\n\n        public void add(String text) {\n            html.add(text);\n        }\n\n        public void end(String text) {\n            if (text.isEmpty()) {\n                html.add(\"</\" + tag + \">\");\n            }\n            html.add(text);\n        }\n\n        public String sanitize() {\n            return String.join(\"\", html)\n                    .replaceAll(\"</?tt>\", \"`\")\n                    .replaceAll(\"</?b>\", \"**\")\n                    .replaceAll(\"</?i>\", \"*\")\n                    .replaceAll(\"<h1>\", \"# \")\n                    .replaceAll(\"<h2>\", \"## \")\n                    .replaceAll(\"<h3>\", \"### \")\n                    .replaceAll(\"<(p|ol|ul)>\\\\s*\", \"\\n\\n\")\n                    .replaceAll(\"</(ol|ul|h.)>\", \"\\n\")\n                    .replaceAll(\"</(li|p)>\\\\s*\", \"\")\n                    .replaceAll(\"<li>\", \"\\n\" + indent + li)\n                    .replaceAll(\"<a href=\\\"(.*)\\\">(.*)</a>\", \"[$2]($1)\");\n        }\n    }\n\n    public static void main(String[] args) {\n        String docletName = MarkdownDoclet.class.getName();\n\n        String[] docletArgs = new String[] {\n                \"-doclet\", docletName,\n                \"-docletpath\", \"target/classes/\",\n                \"-sourcepath\", \"src/main/java/\",\n                \"dev.ebullient.convert.qute\",\n                \"dev.ebullient.convert.tools.dnd5e.qute\",\n                \"dev.ebullient.convert.tools.pf2e.qute\"\n        };\n        DocumentationTool docTool = ToolProvider.getSystemDocumentationTool();\n        docTool.run(System.in, System.out, System.err, docletArgs);\n    }\n\n    private String replacementFor(String str) {\n        switch (str) {\n            case \"&quot;\":\n                return \"\\\"\";\n            case \"&amp;\":\n                return \"&\";\n            case \"&#39;\":\n                return \"'\";\n            case \"&lt;\":\n                return \"<\";\n            case \"&gt;\":\n                return \">\";\n            default:\n                return str;\n        }\n    }\n\n    private boolean isValidClass(String className) {\n        try {\n            Class.forName(className);\n            return true;\n        } catch (ClassNotFoundException e) {\n            int pos = className.lastIndexOf(\".\");\n            if (pos > -1) {\n                String innerClass = className.substring(0, pos) + \"$\" + className.substring(pos + 1);\n                return isValidClass(innerClass);\n            }\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/io/MarkdownWriter.java",
    "content": "package dev.ebullient.convert.io;\n\nimport static dev.ebullient.convert.StringUtil.toTitleCase;\n\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.SortedSet;\nimport java.util.TreeSet;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\n\nimport dev.ebullient.convert.qute.QuteBase;\nimport dev.ebullient.convert.qute.QuteNote;\nimport io.quarkus.qute.TemplateData;\n\npublic class MarkdownWriter {\n    static final Comparator<FileMap> fileSort = (a, b) -> {\n        if (a.dir.equals(b.dir)) {\n            return a.fileName.compareTo(b.fileName);\n        }\n        return a.dir.compareTo(b.dir);\n    };\n\n    public static final Comparator<IndexEntry> sortEntryByPath = Comparator\n            .comparing(IndexEntry::fileName);\n\n    public static final Comparator<IndexEntry> sortEntryByTitle = Comparator\n            .comparing(IndexEntry::title)\n            .thenComparing(IndexEntry::fileName);\n\n    final Tui tui;\n    final Templates templates;\n    final Path output;\n\n    public MarkdownWriter(Path output, Templates templates, Tui tui) {\n        this.tui = tui;\n        this.output = output;\n        this.templates = templates;\n    }\n\n    public <T extends QuteBase> void writeFiles(Path basePath, List<T> elements, IndexContext ctx) {\n        if (elements.isEmpty()) {\n            return;\n        }\n\n        // Counts and sorted lists (to write index)\n        Map<String, Integer> counts = new HashMap<>();\n        Set<FileMap> fileMappings = new TreeSet<>(fileSort);\n\n        // Find duplicates\n        Map<FileMap, List<T>> pathMap = new HashMap<>();\n        for (T qs : elements) {\n            Path path = basePath.resolve(qs.targetPath()).normalize();\n            FileMap fileMap = new FileMap(qs.title(),\n                    qs.targetFile(),\n                    path,\n                    qs.createIndex());\n\n            pathMap.computeIfAbsent(fileMap, k -> new ArrayList<>()).add(qs);\n\n            Collection<QuteBase> inlineNotes = qs.inlineNotes();\n            for (QuteBase n : inlineNotes) {\n                FileMap fm = new FileMap(n.title(),\n                        n.targetFile(),\n                        path,\n                        false);\n\n                pathMap.computeIfAbsent(fm, k -> new ArrayList<>()).add(qs);\n            }\n        }\n\n        for (Map.Entry<FileMap, List<T>> pathEntry : pathMap.entrySet()) {\n            if (pathEntry.getValue().size() > 1) {\n                tui.warnf(\"Conflict: several entries would write to the same file: (%s)\\n  %s\",\n                        pathEntry.getKey().fileName,\n                        pathEntry.getValue().stream().map(x -> String.format(\"[%s]: %s\",\n                                x.getName(),\n                                x.sources().getKey()))\n                                .collect(Collectors.joining(\"\\n  \")));\n            }\n            fileMappings.add(doWrite(pathEntry.getKey(), pathEntry.getValue().get(0), counts));\n        }\n\n        // Accumulate index entries and track distinct dirs for parent rollup\n        Set<Path> indexedDirs = new LinkedHashSet<>();\n        for (FileMap fm : fileMappings) {\n            if (fm.renderIndex) {\n                ctx.accumulateEntry(fm.dir, new IndexEntry(fm.title, fm.fileName, \"./\" + fm.fileName));\n                indexedDirs.add(fm.dir);\n            }\n        }\n\n        // Parent rollup: add each subfolder as an entry in its parent dir.\n        // e.g., compendium/bestiary/beast → adds \"Beast\" entry into compendium/bestiary/\n        for (Path subdir : indexedDirs) {\n            if (subdir.getParent() != null && !subdir.getParent().equals(basePath)) {\n                String dirName = subdir.getFileName().toString();\n                String title = dirName.substring(0, 1).toUpperCase() + dirName.substring(1);\n                ctx.accumulateEntry(subdir.getParent(),\n                        new IndexEntry(title, dirName + \".md\", \"./\" + dirName + \"/\" + dirName + \".md\"));\n            }\n        }\n\n        counts.forEach((k, v) -> tui.printlnf(Msg.OK, \"Wrote %s %s files.\", v, k));\n    }\n\n    <T extends QuteBase> FileMap doWrite(FileMap fileMap, T qs, Map<String, Integer> counts) {\n        try {\n            writeFile(fileMap, templates.render(qs));\n            counts.compute(qs.indexType().name(), (k, v) -> (v == null) ? 1 : v + 1);\n        } catch (IOException e) {\n            throw new UncheckedIOException(e);\n        }\n        return fileMap;\n    }\n\n    void writeFile(FileMap fileMap, String content) throws IOException {\n        Path targetDir = Paths.get(output.toString(), fileMap.dir.toString());\n        targetDir.toFile().mkdirs();\n\n        Path target = targetDir.resolve(fileMap.fileName);\n        Files.write(target, content.getBytes(StandardCharsets.UTF_8));\n    }\n\n    public void writeNotes(Path dir, Collection<QuteNote> notes, boolean compendium, IndexContext ctx) {\n        if (notes.isEmpty()) {\n            return;\n        }\n        Path targetDir = output.resolve(dir);\n\n        for (QuteNote n : notes) {\n            String fn = n.targetFile();\n            Path fd = targetDir.resolve(n.targetPath()).normalize();\n            fd.toFile().mkdirs();\n            String fileName = Tui.slugify(fn) + (fn.endsWith(\".md\") ? \"\" : \".md\");\n            String relative = dir.resolve(n.targetPath()).normalize().toString().replace(\"\\\\\", \"/\");\n            if (relative.isBlank() || relative.equals(\".\")) {\n                relative = \"\";\n            } else {\n                relative += \"/\";\n            }\n            n.vaultPath(relative + fileName);\n            writeNote(fd, fileName, n);\n\n            // Accumulate index entries using vault-relative paths\n            Path relFd = dir.resolve(n.targetPath()).normalize();\n            String dirName = relFd.getFileName().toString();\n\n            // A note \"owns\" its folder if its slugified name matches the folder name.\n            // e.g., traits/traits.md, conditions/conditions.md, pf2e core-rulebook/core-rulebook.md\n            boolean isOwnFolderNote = fileName.equals(dirName + \".md\") && !relFd.equals(dir);\n\n            if (isOwnFolderNote) {\n                // Protect: writeIndexes must not overwrite this note\n                ctx.protectDir(relFd);\n                // Add it as an entry in its parent (if parent is not the base)\n                if (relFd.getParent() != null && !relFd.getParent().equals(dir)) {\n                    ctx.accumulateEntry(relFd.getParent(),\n                            new IndexEntry(n.title(), dirName + \".md\", \"./\" + dirName + \"/\" + fileName));\n                }\n            } else if (!relFd.equals(dir)) {\n                // Normal note in a subdirectory — add to that dir's index\n                ctx.accumulateEntry(relFd, new IndexEntry(n.title(), fileName, \"./\" + fileName));\n                // Parent rollup: books/<slug>/ch.md → add <slug>.md entry to books/\n                if (relFd.getParent() != null && !relFd.getParent().equals(dir)) {\n                    String fdDirName = relFd.getFileName().toString();\n                    String fdTitle = ctx.toTitle(fdDirName);\n                    ctx.accumulateEntry(relFd.getParent(),\n                            new IndexEntry(fdTitle, fdDirName + \".md\",\n                                    \"./\" + fdDirName + \"/\" + fdDirName + \".md\"));\n                }\n            }\n            // Notes at base (relFd == dir) are not indexed\n        }\n\n        tui.printlnf(Msg.OK, \"Wrote %s notes to %s.\",\n                notes.size(),\n                compendium ? \"compendium\" : \"rules\");\n    }\n\n    public void writeIndexes(IndexContext ctx) {\n        for (Map.Entry<Path, SortedSet<IndexEntry>> entry : ctx.accumulator.entrySet()) {\n            Path relDir = entry.getKey();\n            SortedSet<IndexEntry> entries = entry.getValue();\n            if (ctx.protectedDirs.contains(relDir) || entries.isEmpty()) {\n                continue;\n            }\n            String fileName = relDir.getFileName().toString();\n            String title = ctx.toTitle(fileName);\n            String vaultPath = relDir.resolve(fileName).toString().replace('\\\\', '/');\n            try {\n                writeFile(new FileMap(title, fileName, relDir, false),\n                        templates.renderIndex(title, vaultPath, entries));\n            } catch (IOException ex) {\n                throw new UncheckedIOException(ex);\n            }\n        }\n    }\n\n    public static String toTitle(String dirName) {\n        return toTitleCase(dirName.replace(\"-\", \" \"), false);\n    }\n\n    private void writeNote(Path targetDir, String fileName, QuteNote n) {\n        Path target = targetDir.resolve(fileName);\n        String content = templates.render(n);\n        try {\n            Files.write(target, content.getBytes(StandardCharsets.UTF_8));\n        } catch (IOException e) {\n            throw new UncheckedIOException(e);\n        }\n    }\n\n    public static class IndexContext {\n        // vault-relative path → sorted IndexEntries for that dir's folder note\n        final Map<Path, SortedSet<IndexEntry>> accumulator = new HashMap<>();\n        // vault-relative dirs where a folder note was already written (must not be overwritten)\n        final Set<Path> protectedDirs = new HashSet<>();\n\n        final Function<String, String> titleTransform;\n        final Function<Path, Comparator<IndexEntry>> sortOrder;\n\n        public IndexContext(Function<String, String> titleTransform, Function<Path, Comparator<IndexEntry>> sortOrder) {\n            this.titleTransform = titleTransform;\n            this.sortOrder = sortOrder;\n        }\n\n        public String toTitle(String fileName) {\n            return this.titleTransform.apply(fileName);\n        }\n\n        public void accumulateEntry(Path relativeDir, IndexEntry entry) {\n            final var sortBy = sortOrder.apply(relativeDir);\n            accumulator.computeIfAbsent(relativeDir, k -> new TreeSet<>(sortBy)).add(entry);\n        }\n\n        public void protectDir(Path relativeDir) {\n            protectedDirs.add(relativeDir);\n        }\n    }\n\n    @TemplateData\n    public record IndexEntry(String title, String fileName, String relativePath) {\n\n        @Override\n        public String toString() {\n            return \"IndexEntry [title=\" + title + \", fileName=\" + fileName + \", relativePath=\" + relativePath + \"]\";\n        }\n    }\n\n    @TemplateData\n    public static class FileMap {\n\n        public final String title;\n        public final String fileName;\n        public final Path dir;\n        public final boolean renderIndex;\n\n        public FileMap(String title, String fileName, Path dirName, boolean renderIndex) {\n            this.title = title;\n            this.fileName = Tui.slugify(fileName) + (fileName.endsWith(\".md\") ? \"\" : \".md\");\n            this.dir = dirName;\n            this.renderIndex = renderIndex;\n        }\n\n        @Override\n        public String toString() {\n            return \"FileMap [title=\" + title + \", fileName=\" + fileName + \", dir=\" + dir + \"]\";\n        }\n\n        @Override\n        public int hashCode() {\n            final int prime = 31;\n            int result = 1;\n            result = prime * result + ((fileName == null) ? 0 : fileName.hashCode());\n            result = prime * result + ((dir == null) ? 0 : dir.hashCode());\n            return result;\n        }\n\n        @Override\n        public boolean equals(Object obj) {\n            if (this == obj)\n                return true;\n            if (obj == null)\n                return false;\n            if (getClass() != obj.getClass())\n                return false;\n\n            FileMap other = (FileMap) obj;\n            if (fileName == null) {\n                if (other.fileName != null) {\n                    return false;\n                }\n            } else if (!fileName.equals(other.fileName)) {\n                return false;\n            }\n\n            if (dir == null) {\n                return other.dir == null;\n            }\n            return dir.equals(other.dir);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/io/Msg.java",
    "content": "package dev.ebullient.convert.io;\n\npublic enum Msg {\n    ALLDONE(Character.toString(0x1F389)), // 🎉\n    BREW(Character.toString(0x1F37A)), // 🍺\n    CLASSES(Character.toString(0x1F913)), // 🤓\n    DEBUG(Character.toString(0x1F527), \"faint\"), // 🔧\n    DECK(Character.toString(0x1F0CF)), // 🃏\n    DEITY(Character.toString(0x1F47C)), // 👼\n    ERR(Character.toString(0x1F6D1) + \"  ERR|\"), // 🛑\n    FEATURE(Character.toString(0x2B50)), // ⭐️\n    FEATURETYPE(Character.toString(0x1F31F)), // 🌟\n    FILTER(Character.toString(0x1F50D)), // 🔍\n    FOLDER(Character.toString(0x1F4C1)), // 📁\n    ITEM(Character.toString(0x1F9F8)), // 🧸\n    MULTIPLE(Character.toString(0x1F4DA)), // 📚\n    NOT_SET(Character.toString(0x1FAE5) + \" \"), // 🫥\n    OK(Character.toString(0x2705) + \"   OK|\"), // ✅\n    INFO(Character.toString(0x1F537) + \" INFO|\"), // 🔷\n    PROGRESS(Character.toString(0x23F3)), // ⏳\n    RACES(Character.toString(0x1F4D5)), // 📕\n    REPRINT(Character.toString(0x1F4F0)), // 📰\n    SOMEDAY(Character.toString(0x1F6A7)), // 🚧\n    SOURCE(Character.toString(0x1F4D8)), // 📘\n    SPELL(Character.toString(0x1F4AB)), // 💫\n    TARGET(Character.toString(0x1F3AF)), // 🎯\n    UNKNOWN(Character.toString(0x1F47B)), // 👻\n    UNRESOLVED(Character.toString(0x1FAE3)), // 🫣\n    VERBOSE(Character.toString(0x1F537) + \"     |\"), // 🔷\n    WRITING(Character.toString(0x1F5A8) + \" \"), // 🖨️\n    WARN(Character.toString(0x1F538) + \" WARN|\"),\n    NOOP(\"\");\n\n    final String prefix;\n    final String colorPrefix;\n\n    private Msg(String prefix) {\n        this.prefix = prefix + \" \";\n        this.colorPrefix = null;\n    }\n\n    private Msg(String prefix, String color) {\n        this.prefix = prefix + \" \";\n        this.colorPrefix = \"@|%s %s\".formatted(color, prefix);\n    }\n\n    public String color(String message) {\n        if (colorPrefix != null) {\n            return colorPrefix + message + \"|@\";\n        }\n        return wrap(message);\n    }\n\n    public String wrap(String message) {\n        return this == NOOP\n                ? message\n                : prefix + message;\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/io/NoStackTraceException.java",
    "content": "package dev.ebullient.convert.io;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class NoStackTraceException extends RuntimeException {\n    private static final long serialVersionUID = 1L;\n\n    public NoStackTraceException(Throwable cause) {\n        super(flattenMessage(cause));\n    }\n\n    @Override\n    public synchronized Throwable fillInStackTrace() {\n        return null;\n    }\n\n    private static String flattenMessage(Throwable cause) {\n        List<String> sb = new ArrayList<>();\n        while (cause != null) {\n            sb.add(cause.toString());\n            cause = cause.getCause();\n        }\n        return String.join(\"\\n\", sb);\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/io/Templates.java",
    "content": "package dev.ebullient.convert.io;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Base64;\nimport java.util.Collection;\n\nimport jakarta.enterprise.context.ApplicationScoped;\nimport jakarta.inject.Inject;\n\nimport dev.ebullient.convert.config.CompendiumConfig;\nimport dev.ebullient.convert.io.MarkdownWriter.IndexEntry;\nimport dev.ebullient.convert.qute.QuteBase;\nimport dev.ebullient.convert.qute.QuteUtil;\nimport io.quarkus.qute.Engine;\nimport io.quarkus.qute.Template;\nimport io.quarkus.qute.TemplateException;\n\n@ApplicationScoped\npublic class Templates {\n\n    CompendiumConfig config = null;\n\n    @Inject\n    Tui tui;\n\n    @Inject\n    Engine engine;\n\n    public void setCustomTemplates(CompendiumConfig config) {\n        this.config = config;\n        engine.clearTemplates();\n    }\n\n    private Template customTemplateOrDefault(String id) throws RuntimeException {\n        if (config == null) {\n            throw new IllegalStateException(\"Config not set\");\n        }\n\n        String key = config.datasource() + \"/\" + id;\n\n        if (!engine.isTemplateLoaded(key)) {\n            Path customPath = config.getCustomTemplate(id);\n            if (customPath != null) {\n                tui.infof(\"%25s: %s\", id, customPath);\n                try {\n                    Template template = engine.parse(Files.readString(customPath));\n                    engine.putTemplate(key, template);\n                    return template;\n                } catch (IOException e) {\n                    tui.errorf(e, \"Failed reading template for %s from %s\", id, customPath.toAbsolutePath());\n                }\n            }\n            Template tpl = engine.getTemplate(key);\n            if (tpl == null) {\n                tui.errorf(\"Unable to find template for for %s\", key);\n                throw new RuntimeException(\"Unable to render content\");\n            }\n        }\n        return engine.getTemplate(key);\n    }\n\n    public String render(QuteBase resource) {\n        Template tpl = customTemplateOrDefault(resource.template());\n        try {\n            return tpl\n                    .data(\"resource\", resource)\n                    .render()\n                    .replaceAll(\"%%-- .*? --%%\\\\n\", \"\")\n                    .trim();\n        } catch (TemplateException tex) {\n            Throwable cause = tex.getCause();\n            String message = cause != null ? cause.toString() : tex.toString();\n            tui.errorf(tex, message);\n            return \"%% ERROR: \" + message + \" %%\";\n        }\n    }\n\n    public String renderInlineEmbedded(QuteUtil resource) {\n        Template tpl = customTemplateOrDefault(resource.template());\n        try {\n            return tpl\n                    .data(\"resource\", resource)\n                    .render().trim();\n        } catch (TemplateException tex) {\n            Throwable cause = tex.getCause();\n            String message = cause != null ? cause.toString() : tex.toString();\n            tui.errorf(tex, message);\n            return \"%% ERROR: \" + message + \" %%\";\n        }\n    }\n\n    public String renderIndex(String name, String vaultPath, Collection<IndexEntry> resources) {\n        Template tpl = customTemplateOrDefault(\"index.txt\");\n        try {\n            return tpl\n                    .data(\"name\", name)\n                    .data(\"vaultPath\", vaultPath)\n                    .data(\"resources\", resources)\n                    .render();\n        } catch (TemplateException tex) {\n            Throwable cause = tex.getCause();\n            String message = cause != null ? cause.toString() : tex.toString();\n            tui.errorf(tex, message);\n            return \"%% ERROR: \" + message + \" %%\";\n        }\n    }\n\n    public String renderCss(FontRef fontRef, InputStream data) throws IOException {\n        Template tpl = customTemplateOrDefault(\"css-font.txt\");\n        try {\n            String encoded = Base64.getEncoder().encodeToString(data.readAllBytes());\n            int extpos = fontRef.sourcePath.lastIndexOf(\".\");\n            String type = fontRef.sourcePath.substring(extpos + 1);\n            return tpl\n                    .data(\"fontFamily\", fontRef.fontFamily)\n                    .data(\"type\", type)\n                    .data(\"encoded\", encoded)\n                    .render();\n        } catch (TemplateException tex) {\n            Throwable cause = tex.getCause();\n            String message = cause != null ? cause.toString() : tex.toString();\n            tui.errorf(tex, message);\n            return \"%% ERROR: \" + message + \" %%\";\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/io/Tui.java",
    "content": "package dev.ebullient.convert.io;\n\nimport java.io.BufferedInputStream;\nimport java.io.BufferedWriter;\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.PrintStream;\nimport java.io.PrintWriter;\nimport java.net.URI;\nimport java.net.URL;\nimport java.nio.channels.Channels;\nimport java.nio.channels.ReadableByteChannel;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.StandardCopyOption;\nimport java.nio.file.StandardOpenOption;\nimport java.util.Collection;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.TreeSet;\nimport java.util.function.BiConsumer;\nimport java.util.stream.Stream;\n\nimport jakarta.enterprise.context.ApplicationScoped;\nimport jakarta.enterprise.event.Observes;\n\nimport org.yaml.snakeyaml.DumperOptions;\nimport org.yaml.snakeyaml.DumperOptions.FlowStyle;\nimport org.yaml.snakeyaml.DumperOptions.ScalarStyle;\nimport org.yaml.snakeyaml.Yaml;\nimport org.yaml.snakeyaml.nodes.Tag;\nimport org.yaml.snakeyaml.representer.Representer;\n\nimport com.fasterxml.jackson.annotation.JsonAutoDetect;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.core.util.DefaultIndenter;\nimport com.fasterxml.jackson.core.util.DefaultPrettyPrinter;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.MapperFeature;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.introspect.VisibilityChecker;\nimport com.fasterxml.jackson.databind.json.JsonMapper;\nimport com.fasterxml.jackson.dataformat.yaml.YAMLFactory;\nimport com.fasterxml.jackson.dataformat.yaml.YAMLFactoryBuilder;\nimport com.github.slugify.Slugify;\n\nimport dev.ebullient.convert.VersionProvider;\nimport dev.ebullient.convert.config.Datasource;\nimport dev.ebullient.convert.config.TtrpgConfig;\nimport dev.ebullient.convert.config.TtrpgConfig.Fix;\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.qute.QuteUtil;\nimport io.quarkus.runtime.ShutdownEvent;\nimport picocli.CommandLine;\nimport picocli.CommandLine.Help;\nimport picocli.CommandLine.Help.Ansi;\nimport picocli.CommandLine.Help.Ansi.Text;\nimport picocli.CommandLine.Help.ColorScheme;\nimport picocli.CommandLine.Model.CommandSpec;\nimport picocli.CommandLine.ParameterException;\n\n@ApplicationScoped\npublic class Tui {\n    static Tui instance;\n\n    public static Tui instance() {\n        return instance;\n    }\n\n    public final static TypeReference<List<String>> LIST_STRING = new TypeReference<>() {\n    };\n    public final static TypeReference<List<Integer>> LIST_INT = new TypeReference<>() {\n    };\n    public final static TypeReference<Map<String, String>> MAP_STRING_STRING = new TypeReference<>() {\n    };\n    public final static TypeReference<Map<String, List<String>>> MAP_STRING_LIST_STRING = new TypeReference<>() {\n    };\n\n    public final static PrintWriter streamToWriter(PrintStream stream) {\n        return new PrintWriter(stream, true, StandardCharsets.UTF_8);\n    }\n\n    public final static ObjectMapper MAPPER = initMapper(JsonMapper.builder()\n            .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)\n            .build());\n\n    private static Slugify slugify;\n\n    static Slugify slugifier() {\n        Slugify s = slugify;\n        if (s == null) {\n            slugify = s = Slugify.builder()\n                    .customReplacement(\"\\\"\", \"\")\n                    .customReplacement(\"'\", \"\")\n                    .customReplacement(\",\", \"\")\n                    .lowerCase(true)\n                    .build();\n        }\n        return s;\n    }\n\n    public static ObjectMapper mapper(Path p) {\n        return p.getFileName().toString().endsWith(\".json\") ? MAPPER : yamlMapper();\n    }\n\n    private static ObjectMapper yamlMapper;\n\n    private static ObjectMapper yamlMapper() {\n        if (yamlMapper == null) {\n            DumperOptions options = new DumperOptions();\n            options.setDefaultScalarStyle(ScalarStyle.PLAIN);\n            options.setDefaultFlowStyle(FlowStyle.AUTO);\n            options.setPrettyFlow(true);\n            options.setIndent(4);\n            options.setIndicatorIndent(4);\n            options.setIndentWithIndicator(true);\n\n            yamlMapper = initMapper(new ObjectMapper(new YAMLFactoryBuilder(new YAMLFactory())\n                    .dumperOptions(options).build()))\n                    .setDefaultPropertyInclusion(JsonInclude.Include.NON_DEFAULT);\n        }\n        return yamlMapper;\n    }\n\n    private static ObjectMapper initMapper(ObjectMapper mapper) {\n        mapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_DEFAULT)\n                .setVisibility(VisibilityChecker.Std.defaultInstance()\n                        .with(JsonAutoDetect.Visibility.ANY));\n        return mapper;\n    }\n\n    private static Yaml plainYaml;\n\n    public static Yaml plainYaml() {\n        Yaml y = plainYaml;\n        if (y == null) {\n            DumperOptions options = new DumperOptions();\n            options.setDefaultScalarStyle(ScalarStyle.PLAIN);\n            options.setDefaultFlowStyle(FlowStyle.BLOCK);\n            options.setPrettyFlow(true);\n\n            Representer representer = new Representer(options);\n            representer.addClassTag(dev.ebullient.convert.qute.NamedText.class, Tag.MAP); //\n\n            y = plainYaml = new Yaml(representer, options);\n        }\n        return y;\n    }\n\n    private static Yaml quotedYaml;\n\n    public static Yaml quotedYaml() {\n        Yaml y = quotedYaml;\n        if (y == null) {\n            DumperOptions options = new DumperOptions();\n            options.setDefaultScalarStyle(ScalarStyle.DOUBLE_QUOTED);\n            options.setPrettyFlow(true);\n            options.setSplitLines(true);\n            options.setIndent(2);\n            options.setIndicatorIndent(2);\n            options.setIndentWithIndicator(true);\n\n            Representer representer = new Representer(options);\n            representer.addClassTag(dev.ebullient.convert.qute.NamedText.class, Tag.MAP); //\n\n            y = quotedYaml = new Yaml(representer, options);\n        }\n        return y;\n    }\n\n    public static String slugify(String s) {\n        return slugifier().slugify(s);\n    }\n\n    static final boolean picocliDebugEnabled = \"DEBUG\".equalsIgnoreCase(System.getProperty(\"picocli.trace\"));\n\n    Ansi ansi;\n    ColorScheme colors;\n\n    PrintWriter log;\n    PrintWriter out;\n    PrintWriter err;\n\n    private Templates templates;\n    private CommandLine commandLine;\n    private boolean debug;\n    private boolean debugOrLog;\n    private boolean verbose;\n    private boolean verboseOrLog;\n    private Path output = Paths.get(\"\");\n    private final Set<Path> inputRoot = new TreeSet<>();\n\n    public Tui() {\n        this.ansi = Help.Ansi.OFF;\n        this.colors = Help.defaultColorScheme(ansi);\n\n        this.out = streamToWriter(System.out);\n        this.err = streamToWriter(System.err);\n        this.debug = false;\n        this.verbose = true;\n\n        Tui.instance = this;\n    }\n\n    public void init(CommandSpec spec, boolean debug, boolean verbose) {\n        init(spec, debug, verbose, false);\n    }\n\n    public void init(CommandSpec spec, boolean debug, boolean verbose, boolean log) {\n        if (spec != null) {\n            this.ansi = spec.commandLine().getHelp().ansi();\n            this.colors = spec.commandLine().getHelp().colorScheme();\n            this.out = spec.commandLine().getOut();\n            this.err = spec.commandLine().getErr();\n            this.commandLine = spec.commandLine();\n        }\n\n        this.debug = debug || picocliDebugEnabled;\n        this.debugOrLog = this.debug || log;\n        this.verbose = verbose || debug;\n        this.verboseOrLog = this.verbose || log;\n\n        if (log) {\n            Path p = Path.of(\"ttrpg-convert.out.txt\");\n            try {\n                BufferedWriter writer = Files.newBufferedWriter(p, StandardCharsets.UTF_8,\n                        StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);\n                this.log = new PrintWriter(writer, true);\n\n                VersionProvider vp = new VersionProvider();\n                List.of(vp.getVersion()).forEach(this.log::println);\n            } catch (IOException e) {\n                errorf(e, \"Unable to open log file %s: %s\", p.toAbsolutePath(), e.toString());\n            }\n        }\n    }\n\n    void onShutdown(@Observes ShutdownEvent event) {\n        this.close();\n    }\n\n    public void setOutputPath(Path output) {\n        this.output = output;\n    }\n\n    public void setTemplates(Templates templates) {\n        this.templates = templates;\n    }\n\n    public void close() {\n        flush();\n        if (this.log != null) {\n            log.close();\n        }\n    }\n\n    public void flush() {\n        out.flush();\n        err.flush();\n        if (log != null) {\n            log.flush();\n        }\n    }\n\n    private void outLine(String text, Text line) {\n        out.println(line);\n        if (log != null) {\n            log.println(text);\n        }\n    }\n\n    private void errLine(String text, Text line) {\n        err.println(line);\n        if (log != null) {\n            log.println(text);\n        }\n    }\n\n    public boolean isDebug() {\n        return debugOrLog;\n    }\n\n    public boolean isVerbose() {\n        return verboseOrLog;\n    }\n\n    public void debugf(String output, Object... params) {\n        debugf(Msg.NOOP, output, params);\n    }\n\n    public void debugf(Msg msg, String output, Object... params) {\n        if (debugOrLog) {\n            output = format(msg.wrap(output), params);\n            if (debug) {\n                out.println(ansi.new Text(Msg.DEBUG.color(output), colors));\n            }\n            if (log != null) {\n                log.println(Msg.DEBUG.wrap(output));\n            }\n        }\n    }\n\n    public void verbosef(String output, Object... params) {\n        verbosef(Msg.NOOP, output, params);\n    }\n\n    public void verbosef(Msg msg, String output, Object... params) {\n        if (verboseOrLog) {\n            output = format(Msg.VERBOSE.wrap(msg.wrap(output)), params);\n            if (verbose) {\n                out.println(ansi.new Text(output));\n            }\n            if (log != null) {\n                log.println(output);\n            }\n        }\n    }\n\n    public void log(Throwable t, boolean keepException) {\n        if (log != null) {\n            log.println(captureStackTrace(t, keepException));\n        }\n    }\n\n    public void logf(String output, Object... params) {\n        logf(Msg.NOOP, output, params);\n    }\n\n    public void logf(Msg msg, String output, Object... params) {\n        if (log != null) {\n            output = format(msg.wrap(output), params);\n            log.println(output);\n            if (msg == Msg.UNKNOWN || msg == Msg.UNRESOLVED) {\n                log(new Exception(output), false);\n            }\n        }\n    }\n\n    public void progressf(String output, Object... params) {\n        infof(Msg.PROGRESS, output, params);\n    }\n\n    public void infof(Msg msg, String output, Object... params) {\n        infof(msg.wrap(output), params);\n    }\n\n    public void infof(String output, Object... params) {\n        output = format(Msg.INFO.wrap(output), params);\n        outLine(output, ansi.new Text(output));\n    }\n\n    public void warnf(Msg msg, String output, Object... params) {\n        warnf(msg.wrap(output), params);\n    }\n\n    public void warnf(String output, Object... params) {\n        output = format(Msg.WARN.wrap(output), params);\n        outLine(output, ansi.new Text(output));\n    }\n\n    public void printlnf(Msg msgType, String output, Object... params) {\n        output = format(msgType.wrap(output), params);\n        outLine(output, ansi.new Text(output));\n    }\n\n    public void errorf(String output, Object... params) {\n        errorf(null, Msg.NOOP, output, params);\n    }\n\n    public void errorf(Msg msgType, String output, Object... params) {\n        errorf(null, msgType, output, params);\n    }\n\n    public void errorf(Throwable th, String output, Object... params) {\n        errorf(th, Msg.NOOP, output, params);\n    }\n\n    public void errorf(Throwable th, Msg msgType, String output, Object... params) {\n        output = format(msgType.wrap(output), params);\n        error(th, output);\n    }\n\n    private void error(Throwable ex, String errorMsg) {\n        String message = Msg.ERR.wrap(errorMsg\n                .replace(\"java.nio.file.NoSuchFileException: \", \"File not found: \"));\n        errLine(message, colors.errorText(message));\n        if (ex != null && log != null) {\n            log.println(captureStackTrace(ex, true));\n        }\n    }\n\n    private String format(String output, Object... params) {\n        if (params != null && params.length > 0) {\n            return String.format(output, params);\n        }\n        return output;\n    }\n\n    public void throwInvalidArgumentException(String message) {\n        if (commandLine != null) {\n            throw new ParameterException(commandLine, message);\n        } else {\n            throw new IllegalArgumentException(message);\n        }\n    }\n\n    public Optional<Path> resolvePath(Path path) {\n        if (path == null) {\n            return Optional.empty();\n        }\n        // find the right source root (there could be several)\n        return inputRoot.stream()\n                .filter(x -> x.resolve(path).toFile().exists())\n                .findFirst();\n    }\n\n    public void copyFonts(Collection<FontRef> fonts) {\n        for (FontRef fontRef : fonts) {\n            Path targetPath = output.resolve(Path.of(\"css-snippets\", slugify(fontRef.fontFamily) + \".css\"));\n            targetPath.getParent().toFile().mkdirs();\n\n            verbosef(Msg.WRITING, \"Generating CSS snippet for %s\", fontRef.sourcePath);\n            if (fontRef.sourcePath.startsWith(\"http\")) {\n                try (InputStream is = URI.create(fontRef.sourcePath.replace(\" \", \"%20\")).toURL().openStream()) {\n                    Files.writeString(targetPath, templates.renderCss(fontRef, is));\n                } catch (IOException e) {\n                    errorf(\"Unable to copy font. %s\", e);\n                }\n            } else {\n                Optional<Path> resolvedSource = resolvePath(Path.of(fontRef.sourcePath));\n                if (resolvedSource.isEmpty()) {\n                    errorf(\"Unable to find font '%s'\", fontRef.sourcePath);\n                    continue;\n                }\n                try (BufferedInputStream is = new BufferedInputStream(Files.newInputStream(resolvedSource.get()))) {\n                    Files.writeString(targetPath, templates.renderCss(fontRef, is));\n                } catch (IOException e) {\n                    errorf(\"Unable to copy font. %s\", e);\n                }\n            }\n        }\n    }\n\n    public void copyImages(Collection<ImageRef> images) {\n        verbosef(Msg.PROGRESS, \"Processing images\");\n\n        for (ImageRef image : images) {\n            Path targetPath = image.targetFilePath() == null\n                    ? null\n                    : output.resolve(image.targetFilePath());\n            if (targetPath == null || targetPath.toFile().exists()) {\n                // remote resources we are not copying, or a target path that already exists\n                continue;\n            }\n            if (image.sourcePath() == null) {\n                copyRemoteImage(image, targetPath);\n                continue;\n            }\n            if (image.sourcePath().toString().startsWith(\"stream/\")) {\n                copyImageResource(image, targetPath);\n                continue;\n            }\n\n            // target path must be pre-resolved to compendium or rules root\n            // so just make sure the image dir exists\n            targetPath.getParent().toFile().mkdirs();\n            try {\n                Files.copy(image.sourcePath(), targetPath, StandardCopyOption.REPLACE_EXISTING);\n            } catch (IOException e) {\n                errorf(\"Unable to copy image. %s\", e);\n            }\n        }\n    }\n\n    private void copyImageResource(ImageRef image, Path targetPath) {\n        String sourcePath = image.sourcePath().toString().replace(\"stream\", \"\");\n        targetPath.getParent().toFile().mkdirs();\n\n        try (InputStream in = TtrpgConfig.class.getResourceAsStream(sourcePath)) {\n            Files.copy(in, targetPath, StandardCopyOption.REPLACE_EXISTING);\n        } catch (IOException e) {\n            errorf(\"Unable to copy resource. %s\", e);\n        }\n    }\n\n    private void copyRemoteImage(ImageRef image, Path targetPath) {\n        targetPath.getParent().toFile().mkdirs();\n\n        String url = image.url();\n        if (url == null) {\n            errorf(\"ImageRef %s has no URL\", image.targetFilePath());\n            return;\n        }\n        if (!url.startsWith(\"http\") && !url.startsWith(\"file\")) {\n            errorf(\"Remote ImageRef %s has invalid URL %s\", image.targetFilePath(), url);\n            return;\n        }\n\n        Tui.instance().debugf(\"copy image %s\", url);\n        try (ReadableByteChannel readableByteChannel = Channels.newChannel(new URL(url).openStream())) {\n            try (FileOutputStream fileOutputStream = new FileOutputStream(targetPath.toFile())) {\n                fileOutputStream.getChannel().transferFrom(readableByteChannel, 0, Long.MAX_VALUE);\n            }\n        } catch (IOException e) {\n            errorf(\"Unable to copy remote image (%s). \", url, e);\n        }\n    }\n\n    public boolean readFile(Path p, List<Fix> fixes, BiConsumer<String, JsonNode> callback) {\n        inputRoot.add(p.getParent().toAbsolutePath());\n        try {\n            File f = p.toFile();\n            String contents = Files.readString(p);\n            for (Fix fix : fixes) {\n                contents = contents.replaceAll(fix.match, fix.replace);\n            }\n            JsonNode node = MAPPER.readTree(contents);\n            callback.accept(f.getName(), node);\n        } catch (IOException e) {\n            errorf(e, \"Unable to read source file at path %s (%s)\", p, e.getMessage());\n            return false;\n        }\n        return true;\n    }\n\n    public boolean readDirectory(String relative, Path dir, BiConsumer<String, JsonNode> callback) {\n        debugf(Msg.FOLDER.wrap(dir.toString()));\n\n        inputRoot.add(dir.toAbsolutePath());\n\n        boolean result = true;\n        String basename = dir.getFileName().toString();\n        if (\"ancestries\".equals(basename)) {\n            basename = \"ancestry\";\n        } else if (TtrpgConfig.getConfig().datasource() == Datasource.toolsPf2e && \"bestiary\".equals(basename)) {\n            basename = \"creature\";\n        }\n        try (Stream<Path> stream = Files.list(dir)) {\n            Iterator<Path> i = stream.iterator();\n            while (i.hasNext()) {\n                Path p = i.next();\n                File f = p.toFile();\n                String name = p.getFileName().toString();\n                if (f.isDirectory()) {\n                    result &= readDirectory(relative + p.getFileName() + '/', p, callback);\n                } else if ((name.startsWith(\"fluff\") || name.startsWith(basename)) && name.endsWith(\".json\")) {\n                    result &= readFile(p, TtrpgConfig.getFixes(relative + name), callback);\n                }\n            }\n        } catch (Exception e) {\n            errorf(e, \"Error reading %s (%s)\", dir.toString(), e.getMessage());\n            return false;\n        }\n        return result;\n    }\n\n    public boolean readToolsDir(Path toolsBase, BiConsumer<String, JsonNode> callback) {\n        Collection<String> inputs = TtrpgConfig.getFileSources();\n        Collection<String> sourceFiles = TtrpgConfig.getFileSources();\n\n        if (!sourceFiles.stream().allMatch(f -> toolsBase.resolve(f).toFile().exists())) {\n            // Common mistake is to point to the tools directory instead of the data directory\n            Path data = toolsBase.resolve(\"data\");\n            if (data.toFile().isDirectory()) {\n                return readToolsDir(data, callback);\n            } else {\n                debugf(\"Unable to find tools data in %s\", toolsBase.toString());\n                return false;\n            }\n        }\n\n        inputRoot.add(toolsBase.getParent());\n\n        boolean result = true;\n        for (String input : inputs) {\n            Path p = toolsBase.resolve(input);\n            if (p.toFile().isFile()) {\n                result &= readFile(p, TtrpgConfig.getFixes(input), callback);\n            } else {\n                result &= readDirectory(input + \"/\", p, callback);\n            }\n        }\n        return result;\n    }\n\n    public void writeJsonFile(Path outputFile, Map<String, Object> values) throws IOException {\n        DefaultPrettyPrinter pp = new DefaultPrettyPrinter();\n        pp.indentArraysWith(DefaultIndenter.SYSTEM_LINEFEED_INSTANCE);\n        MAPPER.writer()\n                .with(pp)\n                .writeValue(outputFile.toFile(), values);\n    }\n\n    public void writeYamlFile(Path outputFile, Map<String, Object> values) throws IOException {\n        yamlMapper().writer().writeValue(outputFile.toFile(), values);\n    }\n\n    public void writeJsonFile(Path outputFile, Object obj) throws IOException {\n        DefaultPrettyPrinter pp = new DefaultPrettyPrinter();\n        pp.indentArraysWith(DefaultIndenter.SYSTEM_LINEFEED_INSTANCE);\n        MAPPER.writer()\n                .with(pp)\n                .writeValue(outputFile.toFile(), obj);\n    }\n\n    public void writeYamlFile(Path outputFile, Object obj) throws IOException {\n        yamlMapper().writer().writeValue(outputFile.toFile(), obj);\n    }\n\n    public void tryCopyFile(Path source, Path target) {\n        try {\n            Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);\n        } catch (IOException e) {\n            errorf(e, \"Unable to copy file %s to %s\", source.toAbsolutePath(), target.toAbsolutePath());\n        }\n    }\n\n    public String renderEmbedded(QuteUtil resource) {\n        return templates.renderInlineEmbedded(resource);\n    }\n\n    public <T> T readJsonValue(JsonNode node, TypeReference<T> targetRef) {\n        if (node != null) {\n            try {\n                return Tui.MAPPER.convertValue(node, targetRef);\n            } catch (RuntimeException e) {\n                errorf(e, \"Unable to convert %s\", node.toString());\n            }\n        }\n        return null;\n    }\n\n    public <T> T readJsonValue(JsonNode node, Class<T> classTarget) {\n        if (node != null) {\n            try {\n                return Tui.MAPPER.convertValue(node, classTarget);\n            } catch (RuntimeException e) {\n                errorf(e, \"Unable to convert %s\", node.toString());\n            }\n        }\n        return null;\n    }\n\n    public static JsonNode readTreeFromResource(String resource) {\n        try (InputStream in = TtrpgConfig.class.getResourceAsStream(resource)) {\n            return resource.endsWith(\".yaml\")\n                    ? Tui.yamlMapper().readTree(in)\n                    : Tui.MAPPER.readTree(in);\n        } catch (IOException | IllegalArgumentException e) {\n            Tui.instance.errorf(e, \"Unable to read or parse required resource (%s): %s\", resource, e.toString());\n            return null;\n        }\n    }\n\n    public static String jsonStringify(Object o) {\n        return Tui.MAPPER.valueToTree(o).toPrettyString();\n    }\n\n    public static String captureStackTrace(Throwable t, boolean keepException) {\n        var stackTrace = t.getStackTrace();\n        if (stackTrace == null || stackTrace.length == 0) {\n            return keepException ? t.toString() : \"\";\n        }\n        StringBuilder sb = new StringBuilder();\n        if (keepException) {\n            sb.append(Msg.DEBUG.wrap(t.toString())).append(\"\\n\");\n        } else {\n            sb.append(Msg.DEBUG.wrap(t.getMessage())).append(\"\\n\");\n        }\n        for (StackTraceElement e : stackTrace) {\n            if (e.getClassName().startsWith(\"picocli\")) {\n                break;\n            }\n            if (e.getClassName().matches(\".*(Tui|RpgDataConvertCli|writeAll).*\")) {\n                continue;\n            }\n            sb.append(\"\\tat \").append(e.toString()).append(\"\\n\");\n        }\n        return sb.toString();\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/qute/ImageRef.java",
    "content": "package dev.ebullient.convert.qute;\n\nimport java.io.IOException;\nimport java.net.URL;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Path;\n\nimport dev.ebullient.convert.config.TtrpgConfig;\nimport dev.ebullient.convert.config.TtrpgConfig.ImageRoot;\nimport dev.ebullient.convert.io.Tui;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * Create links to referenced images.\n *\n * The general form of a markdown image link is: `![alt text](vaultPath \"title\")`.\n * You can also use anchors to position the image within the page,\n * which creates links that look like this: `![alt text](vaultPath#anchor \"title\")`.\n *\n * ## Anchor Tags\n *\n * Anchor tags are used to position images within a page and are styled with CSS. Examples:\n *\n * - `center` centers the image and constrains its height.\n * - `gallery` constrains images within a gallery callout.\n * - `portrait` floats an image to the right.\n * - `symbol` floats Deity symbols to the right.\n * - `token` is a smaller image, also floated to the right. Used in statblocks.\n */\n@TemplateData\npublic class ImageRef {\n    final Path sourcePath;\n    final Path targetFilePath;\n    final String url;\n    final Integer width;\n    final String vaultPath;\n\n    /** Descriptive title (or caption) for the image. This can be long. */\n    public final String title;\n    final String titleAttr;\n\n    private ImageRef(String url, Path sourcePath, Path targetFilePath, String title, String vaultPath, Integer width) {\n        this.url = url;\n        this.sourcePath = sourcePath;\n        this.targetFilePath = targetFilePath;\n        title = title == null\n                ? \"\"\n                : title.replaceAll(\"\\\\[(.+?)]\\\\(.+?\\\\)\", \"$1\");\n\n        this.titleAttr = \"\";\n        this.title = escape(title);\n        this.vaultPath = vaultPath;\n        this.width = width;\n    }\n\n    String escape(String s) {\n        return s.replace(\"\\\"\", \"&quot;\");\n    }\n\n    /**\n     * A shortened image title (max 50 characters) for use in markdown links.\n     */\n    public String getShortTitle() {\n        return title;\n    }\n\n    private String titleAttribute() {\n        return titleAttr;\n    }\n\n    /** Path of the image in the vault or url for external images. */\n    public String getVaultPath() {\n        return vaultPath == null ? url : vaultPath;\n    }\n\n    public String getEmbeddedLink(String anchor) {\n        return String.format(\"![%s](%s%s%s)\",\n                getShortTitle(),\n                getVaultPath(),\n                anchor.length() > 0 ? \"#\" + anchor : \"\",\n                titleAttribute());\n    }\n\n    /**\n     * Return an embedded markdown link to the image, using an optional\n     * anchor tag to position the image in the page.\n     * For example: `{resource.image.getEmbeddedLink(\"symbol\")}`\n     *\n     * If the title is longer than 50 characters:\n     * `![{resource.shortTitle}]({resource.vaultPath}#anchor \"{resource.title}\")`,\n     *\n     *\n     * If the title is 50 characters or less:\n     * `![{resource.title}]({resource.vaultPath}#anchor)`,\n     *\n     *\n     * Links will be generated using \"center\" as the anchor by default.\n     *\n     */\n    public String getEmbeddedLink() {\n        String anchor = \"center\";\n        // if (width != null && width < 500) {\n        //     anchor = \"right\";\n        // }\n        return String.format(\"![%s](%s#%s%s)\",\n                getShortTitle(),\n                getVaultPath(),\n                anchor,\n                titleAttribute());\n    }\n\n    @Deprecated\n    public String getEmbeddedLinkWithTitle(String anchor) {\n        return getEmbeddedLink(anchor);\n    }\n\n    @Deprecated\n    public String getCaption() {\n        return title;\n    }\n\n    @Deprecated\n    public String getPath() {\n        return vaultPath;\n    }\n\n    /** Not available in templates */\n    public Path sourcePath() {\n        return sourcePath;\n    }\n\n    /** Not available in templates */\n    public Path targetFilePath() {\n        return targetFilePath;\n    }\n\n    /** Not available in templates */\n    public String url() {\n        return url;\n    }\n\n    public static class Builder {\n        private String sourcePath;\n        private Path relativeTarget;\n        private String title = \"\";\n        private Integer width;\n\n        private String vaultRoot;\n        private Path rootFilePath;\n\n        private String url;\n\n        public Builder setSourcePath(Path sourcePath) {\n            this.sourcePath = sourcePath.toString();\n            return this;\n        }\n\n        public Builder setInternalPath(String sourcePath) {\n            this.sourcePath = sourcePath;\n            return this;\n        }\n\n        public Builder setStreamSource(String glyph) {\n            this.sourcePath = \"stream/\" + glyph;\n            return this;\n        }\n\n        public Builder setTitle(String title) {\n            this.title = title;\n            return this;\n        }\n\n        public Builder setVaultRoot(String vaultRoot) {\n            this.vaultRoot = vaultRoot;\n            return this;\n        }\n\n        public Builder setRootFilepath(Path rootFilePath) {\n            this.rootFilePath = rootFilePath;\n            return this;\n        }\n\n        public Builder setRelativePath(Path relativeTarget) {\n            this.relativeTarget = relativeTarget;\n            return this;\n        }\n\n        public Builder setWidth(Integer width) {\n            this.width = width;\n            return this;\n        }\n\n        public Builder setUrl(String url) {\n            this.url = url;\n            return this;\n        }\n\n        public ImageRef build() {\n            if (url == null && sourcePath == null) {\n                Tui.instance().errorf(\"ImageRef build for internal image called without url or sourcePath set\");\n                return null;\n            }\n\n            final ImageRoot imageRoot = TtrpgConfig.internalImageRoot();\n            String sourceUrl = url == null ? sourcePath.toString() : url;\n\n            // Check for any URL replacements (to replace a not-found-image with a local one, e.g.)\n            // replace backslashes with forward slashes\n            sourceUrl = imageRoot.getFallbackPath(sourceUrl)\n                    .replace('\\\\', '/');\n\n            sourceUrl = java.net.URLDecoder.decode(sourceUrl, StandardCharsets.UTF_8);\n\n            boolean copyToVault = false;\n\n            if (sourceUrl.startsWith(\"http\") || sourceUrl.startsWith(\"file\")) {\n                sourceUrl = sourceUrl.replaceAll(\"^(https?):/+\", \"$1://\");\n                copyToVault = imageRoot.copyExternalToVault();\n            } else if (sourceUrl.startsWith(\"stream/\")) {\n                copyToVault = true;\n            } else {\n                copyToVault = imageRoot.copyInternalToVault();\n                sourceUrl = copyToVault\n                        ? imageRoot.getRootPath() + sourceUrl\n                        : imageRoot.getRootPathUrl() + sourceUrl;\n            }\n\n            boolean localTargetSet = relativeTarget != null && vaultRoot != null && rootFilePath != null;\n            if (localTargetSet && copyToVault) {\n                Path targetFilePath = rootFilePath.resolve(relativeTarget);\n                String vaultPath = String.format(\"%s%s\", vaultRoot,\n                        relativeTarget.toString().replace('\\\\', '/'));\n\n                // remote images to be copied into the vault\n                if (sourceUrl.startsWith(\"http\") || sourceUrl.startsWith(\"file\")) {\n                    return new ImageRef(escapeUrlImagePath(sourceUrl),\n                            null, targetFilePath, title, vaultPath, width);\n                }\n                // local image to be copied into the vault\n                return new ImageRef(null, Path.of(sourceUrl), targetFilePath, title, vaultPath, width);\n            }\n\n            // remote images that are not copied to the vault --> url image ref, no target\n            return new ImageRef(escapeUrlImagePath(sourceUrl),\n                    null, null, title, null, width);\n        }\n\n        public ImageRef build(ImageRef previous) {\n            if (previous != null) {\n                return new ImageRef(previous.url,\n                        previous.sourcePath,\n                        previous.targetFilePath,\n                        title,\n                        previous.vaultPath,\n                        width);\n            } else {\n                return build();\n            }\n        }\n\n        private final static String allowedCharacters = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~/\";\n\n        public static String escapeUrlImagePath(String url) {\n            try {\n                URL urlObject = new URL(url);\n                String path = urlObject.getPath();\n\n                StringBuilder encodedPath = new StringBuilder();\n                for (char ch : path.toCharArray()) {\n                    if (allowedCharacters.indexOf(ch) == -1) {\n                        byte[] bytes = String.valueOf(ch).getBytes(StandardCharsets.UTF_8);\n                        for (byte b : bytes) {\n                            encodedPath.append(String.format(\"%%%02X\", b));\n                        }\n                    } else {\n                        encodedPath.append(ch);\n                    }\n                }\n                return url.replace(path, encodedPath.toString())\n                        .replace(\"/imgur.com\", \"/i.imgur.com\");\n            } catch (IOException e) {\n                Tui.instance().errorf(e, \"Unable to escape URL path %s (%s)\", url, e);\n                return url;\n            }\n        }\n\n        public static final String fixUrl(String sourceUrl) {\n            // Remove escaped characters here (inconsistent escaping in the source URL)\n            sourceUrl = java.net.URLDecoder.decode(sourceUrl, StandardCharsets.UTF_8);\n            return escapeUrlImagePath(sourceUrl);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/qute/NamedText.java",
    "content": "package dev.ebullient.convert.qute;\n\nimport java.util.Collection;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.TreeSet;\n\nimport io.quarkus.qute.TemplateData;\nimport io.quarkus.runtime.annotations.RegisterForReflection;\n\n/**\n * Holder of a name or category and associated descriptive text.\n *\n * This attribute will render itself as labeled elements if you reference it directly.\n */\n@TemplateData\n@RegisterForReflection\npublic class NamedText {\n    public final static Comparator<NamedText> comparator = Comparator.comparing(NamedText::getKey);\n\n    /** Name */\n    public final String name;\n    /** Pre-formatted text description including all nested items */\n    public final String desc;\n    /** List of child elements (mostly for YAML) */\n    public final transient Collection<NamedText> nested;\n\n    public NamedText(String name, String desc) {\n        this.name = name;\n        this.desc = desc;\n        this.nested = List.of();\n    }\n\n    public NamedText(String name, Collection<String> text) {\n        this.name = name == null ? \"\" : name;\n        String body = text == null ? \"\" : String.join(\"\\n\", text);\n        if (body.startsWith(\">\")) {\n            body = \"\\n\" + body;\n        }\n        this.desc = body;\n        this.nested = List.of();\n    }\n\n    public NamedText(String name, Collection<String> text, Collection<NamedText> nested) {\n        this.name = name == null ? \"\" : name;\n        String body = text == null ? \"\" : String.join(\"\\n\", text);\n        if (body.startsWith(\">\")) {\n            body = \"\\n\" + body;\n        }\n        this.desc = body;\n        this.nested = nested;\n    }\n\n    public boolean hasContent() {\n        return !(name.isBlank() && desc.isBlank());\n    }\n\n    /** Alternate accessor for the name */\n    public String getKey() {\n        return name;\n    }\n\n    /** Alternate accessor for the name */\n    public String getCategory() {\n        return name;\n    }\n\n    /** Alternate accessor for formatted/descriptive text */\n    public String getText() {\n        return desc;\n    }\n\n    /** Alternate accessor for formatted/descriptive text */\n    public String getValue() {\n        return desc;\n    }\n\n    public String toString() {\n        if (name.isBlank()) {\n            return desc;\n        }\n        return String.format(\"**%s.** %s\", name, desc);\n    }\n\n    public static class SortedBuilder {\n        Set<NamedText> list = new TreeSet<>(comparator);\n\n        public SortedBuilder add(String name, String text) {\n            list.add(new NamedText(name, text));\n            return this;\n        }\n\n        public boolean isEmpty() {\n            return list.isEmpty();\n        }\n\n        /** Returns a modifiable collection of NamedText (sorted order) */\n        public Collection<NamedText> build() {\n            return list;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/qute/QuteAltNames.java",
    "content": "package dev.ebullient.convert.qute;\n\nimport java.util.List;\n\npublic interface QuteAltNames {\n\n    /** Alternate names. (optional) */\n    default List<String> getAltNames() {\n        return List.of();\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/qute/QuteBase.java",
    "content": "package dev.ebullient.convert.qute;\n\nimport static dev.ebullient.convert.StringUtil.quotedEscaped;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\n\nimport dev.ebullient.convert.io.JavadocVerbatim;\nimport dev.ebullient.convert.tools.CompendiumSources;\nimport dev.ebullient.convert.tools.IndexType;\nimport dev.ebullient.convert.tools.Tags;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * Defines attributes inherited by other Qute templates.\n *\n * Notes created from {@code QuteBase} (or a derivative) will use a specific template\n * for the type. For example, {@code QuteBackground} will use {@code background2md.txt}.\n */\n@TemplateData\npublic class QuteBase implements QuteUtil, QuteAltNames {\n    protected final String name;\n    protected final CompendiumSources sources;\n    protected final String sourceText;\n\n    /** Formatted text. For most templates, this is the bulk of the content. */\n    public final String text;\n\n    /** Collected tags for inclusion in frontmatter */\n    public final Collection<String> tags;\n\n    private String vaultPath;\n\n    public QuteBase(CompendiumSources sources, String name, String source, String text, Tags tags) {\n        this.sources = sources;\n        this.name = name;\n        this.sourceText = source;\n        this.text = text;\n        this.tags = tags == null ? List.of() : tags.build();\n    }\n\n    /** Note name */\n    public String getName() {\n        return name;\n    }\n\n    /** String describing the content's source(s) */\n    public String getSource() {\n        return sourceText;\n    }\n\n    /**\n     * Aliases for this note, including the note name, as quoted/escaped strings.\n     *\n     * Example values:\n     * - \"+1 All-Purpose Tool\"\n     * - \"Carl \\\"The Elder\\\" Frost\"\n     *\n     * In templates:\n     * ```md\n     * aliases:\n     * {#each resource.aliases}\n     * - {it}\n     * {/each}\n     * ```\n     */\n    @JavadocVerbatim\n    public List<String> getAliases() {\n        var altNames = getAltNames();\n        if (altNames.isEmpty()) {\n            return List.of(quotedEscaped(name));\n        }\n        List<String> aliases = new ArrayList<>();\n        aliases.add(quotedEscaped(name));\n        for (var alt : altNames) {\n            aliases.add(quotedEscaped(alt));\n        }\n        return aliases;\n    }\n\n    /** Formatted string describing the content's source(s): `_Source: &lt;sources&gt;_` */\n    public String getLabeledSource() {\n        return \"_Source: \" + sourceText + \"_\";\n    }\n\n    /** Book sources as list of {@link dev.ebullient.convert.qute.SourceAndPage} */\n    public Collection<SourceAndPage> getSourceAndPage() {\n        if (sources == null) {\n            return List.of();\n        }\n        return sources.getSourceAndPage();\n    }\n\n    /**\n     * List of source books using abbreviated name. Fantasy statblocks uses this list format, as an example.\n     */\n    public final List<String> getBooks() {\n        return getSourceAndPage().stream()\n                .map(x -> x.source)\n                .toList();\n    }\n\n    /** List of content superceded by this note (as {@link dev.ebullient.convert.qute.Reprinted}) */\n    public Collection<Reprinted> getReprintOf() {\n        if (sources == null) {\n            return List.of();\n        }\n        return sources.getReprints();\n    }\n\n    /**\n     * Get Sources as a footnote.\n     *\n     * Calling this method will return an italicised string with the primary source\n     * followed by a footnote listing all other sources. Useful for types\n     * that tend to have many sources.\n     */\n    public String getSourcesWithFootnote() {\n        if (sources == null) {\n            return \"\";\n        }\n        if (sources.getSources().size() == 1) {\n            SourceAndPage sp = sources.getSourceAndPage().iterator().next();\n            String txt = sp.toString();\n            if (!txt.isEmpty()) {\n                return \"_Source: \" + txt + \"_\";\n            }\n        }\n        String primary = null;\n        List<String> srcTxt = new ArrayList<>();\n        for (var sp : sources.getSourceAndPage()) {\n            String txt = sp.toString();\n            if (!txt.isEmpty()) {\n                if (primary == null) {\n                    primary = txt;\n                } else {\n                    srcTxt.add(txt);\n                }\n            }\n        }\n        return \"_Source: %s_ ^[%s]\".formatted(primary, String.join(\", \", srcTxt));\n    }\n\n    /** True if the content (text) contains sections */\n    public boolean getHasSections() {\n        return text != null && !text.isEmpty() && text.contains(\"\\n## \");\n    }\n\n    public void vaultPath(String vaultPath) {\n        this.vaultPath = vaultPath;\n    }\n\n    /** Path to this note in the vault */\n    public String getVaultPath() {\n        if (vaultPath != null) {\n            return vaultPath;\n        }\n        return targetPath() + '/' + targetFile();\n    }\n\n    public CompendiumSources sources() {\n        return sources;\n    }\n\n    public String title() {\n        return name;\n    }\n\n    public String targetFile() {\n        return name;\n    }\n\n    public String targetPath() {\n        return \".\";\n    }\n\n    @Override\n    public IndexType indexType() {\n        return sources.getType();\n    }\n\n    public String key() {\n        return sources.getKey();\n    }\n\n    public boolean createIndex() {\n        return true;\n    }\n\n    public String template() {\n        IndexType type = indexType();\n        return String.format(\"%s2md.txt\", type.templateName());\n    }\n\n    public Collection<QuteBase> inlineNotes() {\n        return List.of();\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/qute/QuteNote.java",
    "content": "package dev.ebullient.convert.qute;\n\nimport java.util.List;\n\nimport dev.ebullient.convert.tools.CompendiumSources;\nimport dev.ebullient.convert.tools.Tags;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * Common attributes for simple notes. THese attributes are more\n * often used by books, adventures, rules, etc.\n *\n * Notes created from {@code QuteNote} (or a derivative) will look for a template\n * named {@code note2md.txt} by default.\n */\n@TemplateData\npublic class QuteNote extends QuteBase {\n\n    public QuteNote(String name, String sourceText, List<String> text, Tags tags) {\n        super(null, name, sourceText, String.join(\"\\n\", text), tags);\n    }\n\n    public QuteNote(String name, String sourceText, String text, Tags tags) {\n        super(null, name, sourceText, text, tags);\n    }\n\n    public QuteNote(CompendiumSources sources, String name, String sourceText, String text, Tags tags) {\n        super(sources, name, sourceText, text, tags);\n    }\n\n    public String title() {\n        return name;\n    }\n\n    public String targetFile() {\n        return name;\n    }\n\n    public String targetPath() {\n        return \".\";\n    }\n\n    public String template() {\n        return \"note2md.txt\";\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/qute/QuteUtil.java",
    "content": "package dev.ebullient.convert.qute;\n\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\n\nimport dev.ebullient.convert.io.JavadocIgnore;\nimport dev.ebullient.convert.tools.IndexType;\n\n@JavadocIgnore\npublic interface QuteUtil {\n    default boolean isPresent(Map<?, ?> s) {\n        return s != null && !s.isEmpty();\n    }\n\n    default boolean isPresent(Collection<?> s) {\n        return s != null && !s.isEmpty();\n    }\n\n    default boolean isPresent(String s) {\n        return s != null && !s.isBlank();\n    }\n\n    default boolean isPresent(Object s) {\n        return s != null;\n    }\n\n    default void addIntegerUnlessEmpty(Map<String, Object> map, String key, Integer value) {\n        if (value != null) {\n            map.put(key, value);\n        }\n    }\n\n    default void maybeAddBlankLine(List<String> content) {\n        if (content.size() > 0 && !content.get(content.size() - 1).isBlank()) {\n            content.add(\"\");\n        }\n    }\n\n    default void addUnlessEmpty(Map<String, Object> map, String key, String value) {\n        if (value != null && !value.isBlank()) {\n            map.put(key, value);\n        }\n    }\n\n    default void addUnlessEmpty(Map<String, Object> map, String key, Collection<?> value) {\n        if (value != null && !value.isEmpty()) {\n            map.put(key, value);\n        }\n    }\n\n    default String levelToString(String level) {\n        switch (level) {\n            case \"1\":\n                return \"1st\";\n            case \"2\":\n                return \"2nd\";\n            case \"3\":\n                return \"3rd\";\n            default:\n                return level + \"th\";\n        }\n    }\n\n    default String template() {\n        throw new UnsupportedOperationException(\"Tried to call template() on a class which does not have a template defined\");\n    }\n\n    default IndexType indexType() {\n        throw new UnsupportedOperationException(\"Tried to call indexType() on a class which does not have a template defined\");\n    }\n\n    @JavadocIgnore\n    interface Renderable {\n        /** Return this object rendered using its template. */\n        String render();\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/qute/Reprinted.java",
    "content": "package dev.ebullient.convert.qute;\n\nimport io.quarkus.qute.TemplateData;\n\n/**\n * A simple record to hold the name and source of a reprinted item.\n *\n * @param name Name of the reprinted item\n * @param source Primary source of the reprinted item\n */\n@TemplateData\npublic record Reprinted(String name, String source) {\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/qute/SourceAndPage.java",
    "content": "package dev.ebullient.convert.qute;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.config.TtrpgConfig;\nimport dev.ebullient.convert.tools.JsonTextConverter.SourceField;\nimport io.quarkus.qute.TemplateData;\nimport io.quarkus.runtime.annotations.RegisterForReflection;\n\n/**\n * A representation of a source and page number. This attribute will print\n * itself nicely if you don't reference sub-attributes.\n */\n@RegisterForReflection\n@TemplateData\npublic class SourceAndPage {\n    /** Abbreviated source name */\n    public final String source;\n    /** Associated page number (may not be present) */\n    public final String page;\n\n    public SourceAndPage(JsonNode jsonElement) {\n        source = SourceField.source.getTextOrNull(jsonElement);\n        page = SourceField.page.getTextOrNull(jsonElement);\n    }\n\n    public SourceAndPage(String source, String page) {\n        this.source = source;\n        this.page = page;\n    }\n\n    /** Long source name */\n    public String getLongName() {\n        return TtrpgConfig.sourceToLongName(source);\n    }\n\n    public String toString() {\n        if (source != null) {\n            String book = TtrpgConfig.sourceToLongName(source);\n            if (page != null) {\n                return String.format(\"%s p. %s\", book, page);\n            }\n            return book;\n        }\n        return \"\";\n    }\n\n    @Override\n    public int hashCode() {\n        final int prime = 31;\n        int result = 1;\n        result = prime * result + ((source == null) ? 0 : source.hashCode());\n        result = prime * result + ((page == null) ? 0 : page.hashCode());\n        return result;\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n        if (this == obj)\n            return true;\n        if (obj == null)\n            return false;\n        if (getClass() != obj.getClass())\n            return false;\n        SourceAndPage other = (SourceAndPage) obj;\n        if (source == null) {\n            if (other.source != null)\n                return false;\n        } else if (!source.equals(other.source))\n            return false;\n        if (page == null) {\n            return other.page == null;\n        } else\n            return page.equals(other.page);\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/qute/TtrpgTemplateExtension.java",
    "content": "package dev.ebullient.convert.qute;\n\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.function.Function;\n\nimport dev.ebullient.convert.StringUtil;\nimport dev.ebullient.convert.io.JavadocVerbatim;\nimport dev.ebullient.convert.io.Tui;\nimport io.quarkus.qute.TemplateExtension;\n\n/**\n * Qute template extensions for TTRPG data.\n *\n * Use these functions to help render TTRPG data in Qute templates.\n */\n@TemplateExtension\npublic class TtrpgTemplateExtension {\n\n    /**\n     * Return the value formatted with a bonus with a +/- prefix.\n     *\n     * Usage: `{perception.asBonus}`\n     */\n    @JavadocVerbatim\n    static String asBonus(Integer value) {\n        return String.format(\"%+d\", value);\n    }\n\n    /**\n     * Return a Title Case form of this string, capitalizing the first word.\n     * Does not transform the contents of parenthesis (like markdown URLs).\n     *\n     * Usage: `{resource.languages.capitalized}`\n     */\n    @JavadocVerbatim\n    static String capitalized(String s) {\n        return transformText(s, StringUtil::toTitleCase, false);\n    }\n\n    /**\n     * Return a capitalized form of this string, capitalizing the first word of each clause.\n     * Clauses are separated by commas or semicolons. Ignores conjunctions and parenthetical content.\n     *\n     * Usage: `{resource.languages.capitalizedList}`\n     */\n    @JavadocVerbatim\n    static String capitalizedList(String s) {\n        return transformText(s, StringUtil::uppercaseFirst, true);\n    }\n\n    /**\n     * Return the text with a capitalized first letter (ignoring punctuation like '[')\n     *\n     * Usage: `{resource.name.uppercaseFirst}`\n     */\n    @JavadocVerbatim\n    static String uppercaseFirst(String s) {\n        return StringUtil.uppercaseFirst(s);\n    }\n\n    /**\n     * Return the lowercase form of this string.\n     * Does not transform the contents of parenthesis (like markdown URLs).\n     *\n     * Usage: `{resource.name.lowercase}`\n     */\n    @JavadocVerbatim\n    static String lowercase(String s) {\n        return transformText(s, String::toLowerCase, false);\n    }\n\n    /**\n     * Transform the input text by applying the transformer function,\n     * while respecting parentheses (both markdown URL and other parenthetical content)\n     * and optionally handling clauses separated by commas or semicolons.\n     *\n     * @param input\n     * @param transformer\n     * @param byClauses\n     * @return text transformed according to the specified rules\n     */\n    private static String transformText(String input, Function<String, String> transformer, boolean byClauses) {\n        if (input == null || input.isEmpty()) {\n            return input;\n        }\n        StringBuilder out = new StringBuilder();\n        StringBuilder buffer = new StringBuilder();\n        int parenDepth = 0;\n\n        for (int i = 0; i < input.length(); i++) {\n            char c = input.charAt(i);\n            switch (c) {\n                case '(' -> {\n                    if (parenDepth == 0) {\n                        // Flush current buffer before entering parens (like the existing tokenizer)\n                        if (buffer.length() > 0) {\n                            out.append(transformer.apply(buffer.toString()));\n                            buffer.setLength(0);\n                        }\n                    }\n                    parenDepth++;\n                    out.append(c);\n                }\n                case ')' -> {\n                    parenDepth--;\n                    out.append(c);\n                }\n                case ',', ';' -> {\n                    if (parenDepth == 0 && byClauses) {\n                        // End of clause - transform and flush\n                        if (buffer.length() > 0) {\n                            String clause = buffer.toString();\n                            String prefix = clause.startsWith(\"and \")\n                                    ? \"and \"\n                                    : clause.startsWith(\"or \")\n                                            ? \"or \"\n                                            : \"\";\n                            out.append(prefix + transformer.apply(clause.substring(prefix.length())));\n                            buffer.setLength(0);\n                        }\n                        out.append(c);\n\n                        // Handle following whitespace\n                        if (i + 1 < input.length() && Character.isWhitespace(input.charAt(i + 1))) {\n                            i++; // Skip the next character (space)\n                            out.append(input.charAt(i)); // Add the space\n                        }\n                    } else if (parenDepth > 0) {\n                        out.append(c); // Inside parentheses, pass through\n                    } else {\n                        buffer.append(c); // Inside parentheses or not by clauses\n                    }\n                }\n                default -> {\n                    if (parenDepth > 0) {\n                        out.append(c); // Pass through parenthetical content unchanged\n                    } else {\n                        buffer.append(c); // Accumulate transformable content\n                    }\n                }\n            }\n        }\n\n        // Process any remaining buffer content (like the existing tokenizer)\n        if (buffer.length() > 0) {\n            out.append(transformer.apply(buffer.toString()));\n        }\n\n        return out.toString();\n    }\n\n    /**\n     * Return the string pluralized based on the size of the collection.\n     *\n     * Usage: `{resource.name.pluralized(resource.components)}`\n     */\n    @JavadocVerbatim\n    public static String pluralizeLabel(Collection<?> collection, String s) {\n        return StringUtil.pluralize(s, collection.size(), true);\n    }\n\n    /**\n     * Return the given object as a string, with a space prepended if it's non-empty and non-null.\n     *\n     * Usage: `{resource.name.prefixSpace}`\n     */\n    @JavadocVerbatim\n    public static String prefixSpace(Object obj) {\n        if (obj == null) {\n            return \"\";\n        }\n        String s = obj.toString();\n        return s.isEmpty() ? \"\" : (\" \" + s);\n    }\n\n    /**\n     * Return the given collection converted into a string and joined using the specified joiner.\n     *\n     * Usage: `{resource.components.join(\", \")}`\n     */\n    @JavadocVerbatim\n    public static String join(Collection<?> collection, String joiner) {\n        return StringUtil.join(joiner, collection);\n    }\n\n    /**\n     * Return the given list joined into a single string, using a different delimiter for the last element.\n     *\n     * Usage: `{resource.components.joinConjunct(\", \", \" or \")}`\n     */\n    @JavadocVerbatim\n    public static String joinConjunct(Collection<?> collection, String joiner, String lastjoiner) {\n        return StringUtil.joinConjunct(joiner, lastjoiner, collection.stream().map(o -> o.toString()).toList());\n    }\n\n    /**\n     * Return the object as a JSON string\n     *\n     * Usage: `{resource.components.getJsonString(resource)}`\n     */\n    @JavadocVerbatim\n    public static String jsonString(Object o) {\n        return Tui.jsonStringify(o);\n    }\n\n    /**\n     * Skip first element in list\n     *\n     * Usage: `{resource.components.skipFirst}`\n     */\n    @JavadocVerbatim\n    public static List<?> skipFirst(List<?> list) {\n        return list.subList(1, list.size());\n    }\n\n    /**\n     * First element in list\n     *\n     * Usage: `{resource.components.first}`\n     */\n    @JavadocVerbatim\n    public static <T> T first(List<T> list) {\n        return list.get(0);\n    }\n\n    /**\n     * Return the size of a list\n     *\n     * Usage: `{resource.components.size()}`\n     */\n    @JavadocVerbatim\n    public static int size(List<?> list) {\n        return list.size();\n    }\n\n    /**\n     * Escape double quotes in a string (YAML/properties safe)\n     *\n     * Usage: `{resource.components.quotedEscaped}`\n     */\n    @JavadocVerbatim\n    public static String quotedEscaped(String s) {\n        return dev.ebullient.convert.StringUtil.quotedEscaped(s);\n    }\n\n    /**\n     * Escape double quotes in a string (YAML/properties safe)\n     *\n     * Usage: `{resource.components.quotedEscaped}`\n     */\n    @JavadocVerbatim\n    public static String quotedEscaped(Optional<String> s) {\n        return s.map(str -> dev.ebullient.convert.StringUtil.quotedEscaped(str)).orElse(\"\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/qute/package-info.java",
    "content": "/**\n * <h1>Qute Template Reference</h1>\n *\n * The following pages describe attributes that you can use to customize\n * generated output in Qute templates.\n *\n * Use a {@code resource.} prefix to access these attributes unless otherwise noted.\n * For example, {@code resource.title}.\n *\n * For more information about Qute, see the [Qute guide](https://quarkus.io/guides/qute).\n *\n * - [Documentation for using templates with the CLI](../../examples/templates/README.md)\n * - [5e Template Examples](../../examples/templates/tools5e/README.md)\n * - {@link dev.ebullient.convert.tools.dnd5e.qute 5eTools template attributes}\n * - {@link dev.ebullient.convert.tools.pf2e.qute Pf2eTools template attributes}\n */\npackage dev.ebullient.convert.qute;\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/CompendiumSources.java",
    "content": "package dev.ebullient.convert.tools;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashSet;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.config.TtrpgConfig;\nimport dev.ebullient.convert.qute.Reprinted;\nimport dev.ebullient.convert.qute.SourceAndPage;\nimport dev.ebullient.convert.tools.JsonTextConverter.SourceField;\nimport io.quarkus.qute.TemplateData;\n\n@TemplateData\npublic abstract class CompendiumSources {\n    protected final IndexType type;\n    protected final String key;\n    protected final String name;\n\n    // sources will only appear once, iterate by insertion order\n    protected final Set<String> sources = new LinkedHashSet<>();\n    protected final Set<SourceAndPage> bookRef = new LinkedHashSet<>();\n    protected String sourceText;\n\n    // Provides a list of sources that this is a reprint of\n    protected final Set<CompendiumSources> reprintOf = new HashSet<>();\n    // Source that this is a copy of\n    protected final JsonNode copyElement;\n\n    public CompendiumSources(IndexType type, String key, JsonNode jsonElement) {\n        this.type = type;\n        this.key = key;\n        this.name = findName(type, jsonElement);\n        // remember this: handling copies will remove the copy field from the element\n        // to avoid repeated processing\n        this.copyElement = Fields._copy.getFrom(jsonElement);\n        initSources(jsonElement);\n    }\n\n    public String getSourceText() {\n        if (sourceText == null) {\n            sourceText = findSourceText(type, findNode());\n        }\n        return sourceText;\n    }\n\n    public Collection<String> getSources() {\n        return sources;\n    }\n\n    /** Protected: used by Tags.addSourceTags(sources) */\n    String primarySourceTag() {\n        return String.format(\"compendium/src/%s/%s\",\n                TtrpgConfig.getConfig().datasource().shortName(),\n                isSynthetic() ? \"\" : primarySource().toLowerCase());\n    }\n\n    public abstract JsonNode findNode();\n\n    protected abstract String findName(IndexType type, JsonNode jsonElement);\n\n    protected void initSources(JsonNode jsonElement) {\n        // add the primary source...\n        SourceAndPage primary = new SourceAndPage(jsonElement);\n        if (primary.source != null) {\n            this.sources.add(primary.source);\n            this.bookRef.add(primary);\n        } else if (type.defaultSourceString() != null) {\n            // synthetic groups don't have a default source\n            String source = type.defaultSourceString();\n            this.sources.add(source);\n            this.bookRef.add(new SourceAndPage(source, null));\n        }\n\n        String copySrc = SourceField.source.getTextOrNull(copyElement);\n\n        if (Fields.additionalSources.existsIn(jsonElement)) {\n            // Additional information from...\n            Fields.additionalSources.streamFrom(jsonElement)\n                    .map(SourceAndPage::new)\n                    .filter(sp -> sp.source != null)\n                    .filter(sp -> !sp.source.equals(copySrc))\n                    .forEach(sp -> {\n                        this.bookRef.add(sp);\n                        this.sources.add(sp.source);\n                    });\n        }\n\n        if (Fields.otherSources.existsIn(jsonElement)) {\n            // Also found in...\n            // This can be overly generous.. only add other sources that\n            // are explicitly included in the configuration\n            Fields.otherSources.streamFrom(jsonElement)\n                    .map(SourceAndPage::new)\n                    .filter(sp -> sp.source != null)\n                    .filter(sp -> !sp.source.equals(copySrc))\n                    .filter(sp -> TtrpgConfig.getConfig().sourceIncluded(sp.source))\n                    .forEach(sp -> {\n                        this.bookRef.add(sp);\n                        this.sources.add(sp.source);\n                    });\n        }\n    }\n\n    protected String findSourceText(IndexType type, JsonNode jsonElement) {\n        List<String> srcText = new ArrayList<>();\n\n        final SourceAndPage primary = bookRef.iterator().next();\n        List<SourceAndPage> consolidated = bookRef.stream()\n                .reduce(new ArrayList<>(), (list, sp) -> {\n                    if (list.isEmpty()) {\n                        list.add(sp);\n                    } else {\n                        SourceAndPage existing = list.stream()\n                                .filter(x -> x.source.equals(sp.source))\n                                .findFirst()\n                                .orElse(null);\n                        if (existing == null) {\n                            list.add(sp);\n                        } else if (existing.page != null) {\n                            SourceAndPage replace = new SourceAndPage(existing.source, null);\n                            list.remove(existing);\n                            if (existing == primary) {\n                                list.add(0, replace);\n                            } else {\n                                list.add(replace);\n                            }\n                        }\n                    }\n                    return list;\n                }, (a, b) -> {\n                    a.addAll(b);\n                    return a;\n                });\n\n        final SourceAndPage first = consolidated.iterator().next();\n        if (first.source != null) {\n            srcText.add(first.toString());\n        }\n\n        String copyOf = SourceField.name.getTextOrNull(copyElement);\n        String copySrc = SourceField.source.getTextOrNull(copyElement);\n        String copiedFrom = Fields._copiedFrom.getTextOrNull(copyElement);\n\n        if (copyOf != null) {\n            srcText.add(String.format(\"Derived from %s (%s)\", copyOf, copySrc));\n        } else if (copiedFrom != null) {\n            srcText.add(String.format(\"Derived from %s\", copiedFrom));\n        }\n\n        // find/add additional sources\n        consolidated.stream()\n                .filter(sp -> sp != first && sp.source != null)\n                .filter(sp -> !sp.source.equals(copySrc))\n                .forEach(sp -> srcText.add(sp.toString()));\n\n        return String.join(\", \", srcText);\n    }\n\n    public boolean isPrimarySource(String source) {\n        return source.equalsIgnoreCase(primarySource());\n    }\n\n    public String primarySource() {\n        if (sources.isEmpty()) {\n            return type.defaultSourceString();\n        }\n        return sources.iterator().next();\n    }\n\n    public String mapPrimarySource() {\n        String primary = primarySource();\n        return TtrpgConfig.sourceToAbbreviation(primary);\n    }\n\n    public boolean includedBy(Set<String> sources) {\n        return TtrpgConfig.getConfig().allSources()\n                || this.sources.stream().anyMatch(x -> sources.contains(x.toLowerCase()));\n    }\n\n    public String getKey() {\n        return key;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public IndexType getType() {\n        return type;\n    }\n\n    public Collection<SourceAndPage> getSourceAndPage() {\n        return bookRef;\n    }\n\n    public Collection<Reprinted> getReprints() {\n        return reprintOf.stream()\n                .map(s -> new Reprinted(s.getName(), s.primarySource()))\n                .collect(Collectors.toList());\n    }\n\n    @Override\n    public String toString() {\n        return \"sources[\" + key + ']';\n    }\n\n    public void checkKnown() {\n        TtrpgConfig.checkKnown(this.sources);\n    }\n\n    public void addReprint(CompendiumSources reprint) {\n        this.reprintOf.add(reprint);\n    }\n\n    /** Documents that have no primary source (compositions) */\n    protected boolean isSynthetic() {\n        return false;\n    }\n\n    protected enum Fields implements JsonNodeReader {\n        _copy,\n        _copiedFrom,\n        additionalSources,\n        otherSources,\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/IndexType.java",
    "content": "package dev.ebullient.convert.tools;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\npublic interface IndexType {\n\n    String name();\n\n    String templateName();\n\n    String defaultSourceString();\n\n    /** Return the key for the given node in the index. */\n    String createKey(JsonNode node);\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/JsonCopyException.java",
    "content": "package dev.ebullient.convert.tools;\n\npublic class JsonCopyException extends RuntimeException {\n    public JsonCopyException(String message) {\n        super(message);\n    }\n\n    public JsonCopyException(String message, Throwable cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/JsonNodeReader.java",
    "content": "package dev.ebullient.convert.tools;\n\nimport static dev.ebullient.convert.StringUtil.isPresent;\nimport static dev.ebullient.convert.StringUtil.join;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.Optional;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport java.util.stream.StreamSupport;\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.ArrayNode;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\n\nimport dev.ebullient.convert.io.Tui;\n\npublic interface JsonNodeReader {\n\n    String name();\n\n    default String nodeName() {\n        return this.name();\n    }\n\n    default void debugIfExists(JsonNode node, Tui tui) {\n        if (existsIn(node)) {\n            tui.errorf(this.name() + \" is defined in \" + node.toPrettyString());\n        }\n    }\n\n    default void appendUnlessEmptyFrom(JsonNode x, List<String> text) {\n        String value = getTextOrEmpty(x);\n        if (!value.isEmpty()) {\n            text.add(value);\n        }\n    }\n\n    default void appendUnlessEmptyFrom(JsonNode x, List<String> text, JsonTextConverter<?> replacer) {\n        String value = replacer.replaceText(getTextOrEmpty(x));\n        if (!value.isEmpty()) {\n            text.add(value);\n        }\n    }\n\n    default String bonusOrNull(JsonNode x) {\n        JsonNode value = getFrom(x);\n        if (value == null) {\n            return null;\n        }\n        if (!value.isNumber()) {\n            throw new IllegalArgumentException(\"bonusOrNull can only work with numbers: \" + value);\n        }\n        int n = value.asInt();\n        return (n >= 0 ? \"+\" : \"\") + n;\n    }\n\n    /**\n     * Return the boolean value of the field in the node:\n     * - if the field is a boolean, return the value\n     * - if the field is a string, return true if the string is present and not 'false'\n     *\n     * @param source\n     * @param value\n     * @return\n     */\n    default boolean coerceBooleanOrDefault(JsonNode source, boolean value) {\n        JsonNode result = getFrom(source);\n        if (result == null) {\n            return value;\n        }\n        if (result.isBoolean()) {\n            return result.asBoolean();\n        }\n        if (result.isTextual()) {\n            return !result.asText().equalsIgnoreCase(\"false\");\n        }\n        return value;\n    }\n\n    default boolean booleanOrDefault(JsonNode source, boolean value) {\n        JsonNode result = getFrom(source);\n        return result == null ? value : result.asBoolean(value);\n    }\n\n    default Double doubleOrDefault(JsonNode source, double value) {\n        JsonNode result = getFrom(source);\n        return result == null ? value : result.asDouble();\n    }\n\n    default Double doubleOrNull(JsonNode source) {\n        JsonNode result = getFrom(source);\n        return result == null ? null : result.asDouble();\n    }\n\n    default boolean existsIn(JsonNode source) {\n        if (source == null || source.isNull()) {\n            return false;\n        }\n        return source.has(this.nodeName());\n    }\n\n    default boolean nestedExistsIn(JsonNodeReader field, JsonNode source) {\n        if (source == null || source.isNull()) {\n            return false;\n        }\n        JsonNode parent = field.getFrom(source);\n        return this.existsIn(parent);\n    }\n\n    default boolean isArrayIn(JsonNode source) {\n        if (source == null || !source.has(this.nodeName())) {\n            return false;\n        }\n        return source.get(this.nodeName()).isArray();\n    }\n\n    default boolean isObjectIn(JsonNode source) {\n        if (source == null || !source.has(this.nodeName())) {\n            return false;\n        }\n        return source.get(this.nodeName()).isObject();\n    }\n\n    default <T> T fieldFromTo(JsonNode source, Class<T> classTarget, Tui tui) {\n        return tui.readJsonValue(source.get(this.nodeName()), classTarget);\n    }\n\n    default <T> T fieldFromTo(JsonNode source, TypeReference<T> targetRef, Tui tui) {\n        return tui.readJsonValue(source.get(this.nodeName()), targetRef);\n    }\n\n    default JsonNode getFrom(JsonNode source) {\n        if (source == null) {\n            return null;\n        }\n        return source.get(this.nodeName());\n    }\n\n    /**\n     * Return an optional of the object at this key in the node, or an empty optional if the key does not exist or is not an\n     * object.\n     */\n    default Optional<JsonNode> getObjectFrom(JsonNode source) {\n        return this.isObjectIn(source) ? Optional.of(this.getFrom(source)) : Optional.empty();\n    }\n\n    default JsonNode getFromOrEmptyObjectNode(JsonNode source) {\n        if (source == null) {\n            return Tui.MAPPER.createObjectNode();\n        }\n        JsonNode result = source.get(this.nodeName());\n        return result == null\n                ? Tui.MAPPER.createObjectNode()\n                : result;\n    }\n\n    /**\n     * Find the first element in the array of this property.\n     * Useful for elements that are ceremonial arrays (they\n     * are always an array of one element)\n     */\n    default JsonNode getFirstFromArray(JsonNode source) {\n        if (source == null) {\n            return null;\n        }\n        JsonNode result = source.get(this.nodeName());\n        if (result == null || !result.isArray()) {\n            return null;\n        }\n        return result.get(0);\n    }\n\n    default JsonNode getFieldFrom(JsonNode source, JsonNodeReader field) {\n        JsonNode targetNode = getFrom(source);\n        if (targetNode == null) {\n            return null;\n        }\n        return targetNode.get(field.nodeName());\n    }\n\n    default List<String> getListOfStrings(JsonNode source, Tui tui) {\n        JsonNode target = getFrom(source);\n        if (target == null || target.isNull()) {\n            return List.of();\n        }\n        if (target.isTextual()) {\n            return List.of(target.asText());\n        }\n        if (target.isObject()) {\n            throw new IllegalArgumentException(\n                    \"Unexpected object when creating list of strings: %s\".formatted(\n                            source));\n        }\n        List<String> list = fieldFromTo(source, Tui.LIST_STRING, tui);\n        return list == null ? List.of() : list;\n    }\n\n    default Map<String, String> getMapOfStrings(JsonNode source, Tui tui) {\n        JsonNode target = getFrom(source);\n        if (target == null || target.isNull()) {\n            return Map.of();\n        }\n        if (target.isTextual() || target.isArray()) {\n            throw new IllegalArgumentException(\n                    \"Unexpected text or array when creating map of strings: %s\".formatted(\n                            source));\n        }\n        Map<String, String> map = fieldFromTo(source, Tui.MAP_STRING_STRING, tui);\n        return map == null ? Map.of() : map;\n    }\n\n    default String getTextOrDefault(JsonNode x, String value) {\n        String text = getTextOrNull(x);\n        return text == null ? value : text;\n    }\n\n    default String getTextOrEmpty(JsonNode x) {\n        String text = getTextOrNull(x);\n        return text == null ? \"\" : text;\n    }\n\n    default String getTextOrNull(JsonNode x) {\n        JsonNode text = getFrom(x);\n        return text == null || text.isNull() ? null : text.asText();\n    }\n\n    default String getTextOrThrow(JsonNode x) {\n        String text = getTextOrNull(x);\n        if (text == null) {\n            throw new IllegalArgumentException(\"Missing text from \" + this.nodeName());\n        }\n        return text;\n    }\n\n    default Optional<String> getTextFrom(JsonNode x) {\n        if (x != null && existsIn(x) && getFrom(x).isTextual()) {\n            return Optional.of(getFrom(x).asText());\n        }\n        return Optional.empty();\n    }\n\n    default Optional<Integer> intFrom(JsonNode source) {\n        JsonNode result = getFrom(source);\n        return result == null || !result.isInt() ? Optional.empty() : Optional.of(result.asInt());\n    }\n\n    default int intOrDefault(JsonNode source, int value) {\n        JsonNode result = getFrom(source);\n        return result == null || result.isNull() ? value : result.asInt();\n    }\n\n    default Integer intOrNull(JsonNode source) {\n        JsonNode result = getFrom(source);\n        return result == null || result.isNull() ? null : result.asInt();\n    }\n\n    default int intOrThrow(JsonNode x) {\n        JsonNode result = getFrom(x);\n        if (result == null) {\n            throw new IllegalArgumentException(\"Missing int from \" + this.nodeName());\n        }\n        return result.asInt();\n    }\n\n    default String joinAndReplace(JsonNode source, JsonTextConverter<?> replacer) {\n        return joinAndReplace(source, replacer, \", \");\n    }\n\n    default String joinAndReplace(JsonNode source, JsonTextConverter<?> replacer, String join) {\n        JsonNode array = getFrom(source);\n        if (array == null || array.isNull()) {\n            return \"\";\n        } else if (!array.isArray()) {\n            throw new IllegalArgumentException(\"joinAndReplace can only work with arrays: \" + array);\n        }\n        return StreamSupport.stream(array.spliterator(), false)\n                .map(v -> replacer.replaceText(v.asText()))\n                .collect(Collectors.joining(join));\n    }\n\n    default <T extends IndexType> List<String> linkifyListFrom(JsonNode node, T type, JsonTextConverter<T> convert) {\n        List<String> list = getListOfStrings(node, convert.tui());\n        return list.stream().map(s -> convert.linkify(type, s)).toList();\n    }\n\n    default String replaceTextFrom(JsonNode node, JsonTextConverter<?> replacer) {\n        return replacer.replaceText(getTextOrEmpty(node));\n    }\n\n    default List<String> replaceTextFromList(JsonNode node, JsonTextConverter<?> convert) {\n        List<String> list = getListOfStrings(node, convert.tui());\n        return list.stream().map(convert::replaceText).toList();\n    }\n\n    default int size(JsonNode source) {\n        JsonNode target = getFrom(source);\n        if (target == null) {\n            return 0;\n        }\n        return target.size();\n    }\n\n    default Stream<JsonNode> streamFrom(JsonNode source) {\n        JsonNode result = getFrom(source);\n        if (result == null) {\n            return Stream.of();\n        } else if (result.isObject()) {\n            return Stream.of(result);\n        }\n        return StreamSupport.stream(result.spliterator(), false);\n    }\n\n    /** Wrapper around {@link #streamPropsExcluding(JsonNode, JsonNodeReader...)} to make the naming less confusing */\n    default Stream<Entry<String, JsonNode>> streamProps(JsonNode source) {\n        return streamPropsExcluding(source);\n    }\n\n    /** Returns a stream of entries of (key, node) from the given node, excluding the given keys. */\n    default Stream<Entry<String, JsonNode>> streamPropsExcluding(JsonNode source, JsonNodeReader... excludingKeys) {\n        JsonNode result = getFrom(source);\n        if (result == null || !result.isObject()) {\n            return Stream.of();\n        }\n        return result.properties().stream()\n                .filter(e -> Arrays.stream(excludingKeys).noneMatch(s -> e.getKey().equalsIgnoreCase(s.name())));\n    }\n\n    /**\n     * {@link #transformTextFrom(JsonNode, String, JsonTextConverter, String)} with a null heading.\n     *\n     * @see #transformTextFrom(JsonNode, String, JsonTextConverter)\n     */\n    default String transformTextFrom(JsonNode source, String join, JsonTextConverter<?> replacer) {\n        return transformTextFrom(source, join, replacer, null);\n    }\n\n    /**\n     * Read the field in from the source as a potentially-nested array of entries. This calls\n     * {@link JsonTextConverter#appendToText(List, JsonNode, String)} on the input node and returns\n     * the parsed result joined according to the given delimiter.\n     *\n     * @param source The node to read from\n     * @param delimiter The delimiter to use when joining the entries into a single string\n     * @param replacer The {@link JsonTextConverter} to use for parsing entries.\n     * @param heading The heading to pass to {@link JsonTextConverter#appendToText(List, JsonNode, String)}\n     */\n    default String transformTextFrom(JsonNode source, String delimiter, JsonTextConverter<?> replacer, String heading) {\n        JsonNode target = getFrom(source);\n        if (target == null) {\n            return \"\";\n        }\n        List<String> inner = new ArrayList<>();\n        replacer.appendToText(inner, target, heading);\n        return join(delimiter, inner);\n    }\n\n    /**\n     * Parse this field from {@code source} as potentially-nested array of entries, and return a list of strings. This\n     * calls {@link JsonTextConverter#appendToText(List, JsonNode, String)} to recursively parse the input.\n     */\n    default List<String> transformListFrom(JsonNode source, JsonTextConverter<?> convert) {\n        if (!isArrayIn(source)) {\n            return List.of();\n        }\n        List<String> inner = new ArrayList<>();\n        convert.appendToText(inner, getFrom(source), null);\n        return inner;\n    }\n\n    /** Returns the enum value of {@code enumClass} that this field in {@code source} contains, or null. */\n    default <E extends Enum<E>> E getEnumValueFrom(JsonNode source, Class<E> enumClass) {\n        String value = getTextOrNull(source);\n        if (!isPresent(value)) {\n            return null;\n        }\n        try {\n            return Enum.valueOf(enumClass, value.toLowerCase());\n        } catch (IllegalArgumentException ignored) {\n        }\n        try {\n            return Enum.valueOf(enumClass, value.toUpperCase());\n        } catch (IllegalArgumentException ignored) {\n            return null;\n        }\n    }\n\n    /** An enum which implements this interface should have values which represent different JSON field values. */\n    interface FieldValue {\n        String name();\n\n        default String value() {\n            return name();\n        }\n\n        default boolean isValueOfField(JsonNode source, JsonNodeReader field) {\n            return matches(field.getTextOrEmpty(source));\n        }\n\n        default boolean matches(String value) {\n            return this.value().equalsIgnoreCase(value) || this.name().equalsIgnoreCase(value);\n        }\n\n        static <E extends Enum<E> & FieldValue> E valueFrom(String value, Class<E> enumClass) {\n            if (!isPresent(value)) {\n                return null;\n            }\n            return Arrays.stream(enumClass.getEnumConstants()).filter(e -> e.matches(value)).findAny().orElse(null);\n        }\n    }\n\n    default boolean valueEquals(JsonNode previous, JsonNode next) {\n        if (previous == null || next == null) {\n            return true;\n        }\n        JsonNode prevValue = previous.get(this.nodeName());\n        JsonNode nextValue = next.get(this.nodeName());\n        return (prevValue == null && nextValue == null)\n                || (prevValue != null && prevValue.equals(nextValue));\n    }\n\n    /**\n     * Will always return an array (no null checks)\n     * Does not create an array attribute if the element is not present\n     */\n    default ArrayNode readArrayFrom(JsonNode source) {\n        if (isArrayIn(source)) {\n            return source.withArray(this.nodeName());\n        }\n        return Tui.MAPPER.createArrayNode();\n    }\n\n    default Iterable<JsonNode> iterateArrayFrom(JsonNode source) {\n        if (isArrayIn(source)) {\n            return () -> source.withArray(this.nodeName()).elements();\n        }\n        return List.of();\n    }\n\n    default Iterable<Entry<String, JsonNode>> iterateFieldsFrom(JsonNode source) {\n        if (isObjectIn(source)) {\n            return () -> source.get(this.nodeName()).properties().iterator();\n        }\n        return List.of();\n    }\n\n    /**\n     * Will always return an array (no null checks)\n     * Will create an array attribute if the element is not present\n     */\n    default ArrayNode ensureArrayIn(JsonNode target) {\n        if (target == null) {\n            return Tui.MAPPER.createArrayNode();\n        }\n        return target.withArrayProperty(this.nodeName());\n    }\n\n    default JsonNode copyFrom(JsonNode source) {\n        return getFrom(source).deepCopy();\n    }\n\n    /** Destructive! */\n    default JsonNode removeFrom(JsonNode target) {\n        if (target == null) {\n            return null;\n        }\n        return ((ObjectNode) target).remove(this.nodeName());\n    }\n\n    /** Destructive! */\n    default void setIn(JsonNode target, JsonNode value) {\n        ((ObjectNode) target).set(this.nodeName(), value);\n    }\n\n    /** Destructive! */\n    default void setIn(JsonNode target, String value) {\n        ((ObjectNode) target).put(this.nodeName(), value);\n    }\n\n    /** Destructive! */\n    default void setIn(JsonNode target, boolean b) {\n        ((ObjectNode) target).put(this.nodeName(), b);\n    }\n\n    /** Destructive! */\n    default void copy(JsonNode source, JsonNode target) {\n        if (source == null || target == null) {\n            return;\n        }\n        if (!source.has(this.nodeName())) {\n            return;\n        }\n        ((ObjectNode) target).set(this.nodeName(), getFrom(source).deepCopy());\n    }\n\n    /** Destructive! */\n    default void link(JsonNode source, JsonNode target) {\n        if (source == null || target == null) {\n            return;\n        }\n        if (!source.has(this.nodeName())) {\n            return;\n        }\n        ((ObjectNode) target).set(this.nodeName(), getFrom(source));\n    }\n\n    /** Destructive! */\n    default void moveFrom(JsonNode source, JsonNode target) {\n        if (source == null || target == null) {\n            return;\n        }\n        JsonNode value = removeFrom(source);\n        if (value != null) {\n            ((ObjectNode) target).set(this.nodeName(), value);\n        }\n    }\n\n    /** Destructive! */\n    default void appendToArray(JsonNode target, String value) {\n        if (target == null) {\n            return;\n        }\n        ArrayNode array = ensureArrayIn(target).add(value);\n        setIn(target, array);\n    }\n\n    /** Destructive! */\n    default void appendToArray(JsonNode target, JsonNode value) {\n        if (target == null || value == null) {\n            return;\n        }\n        ArrayNode array = ensureArrayIn(target);\n        if (value.isArray()) {\n            array.addAll((ArrayNode) value);\n        } else {\n            array.add(value);\n        }\n        setIn(target, array);\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/JsonSourceCopier.java",
    "content": "package dev.ebullient.convert.tools;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Map.Entry;\nimport java.util.Set;\nimport java.util.TreeSet;\nimport java.util.function.Consumer;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.ArrayNode;\nimport com.fasterxml.jackson.databind.node.DoubleNode;\nimport com.fasterxml.jackson.databind.node.IntNode;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport com.fasterxml.jackson.databind.node.TextNode;\n\nimport dev.ebullient.convert.io.Msg;\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.tools.JsonNodeReader.FieldValue;\n\n/** Performs copy operations on nodes as a pre-processing step before they're handled by the individual converters. */\npublic abstract class JsonSourceCopier<T extends IndexType> implements JsonTextConverter<T> {\n    public static final Pattern VARIABLE_SUBST_PAT = Pattern.compile(\"<\\\\$(?<variable>[^$]+)\\\\$>\");\n\n    /** Return the original node for the given key. */\n    protected abstract JsonNode getOriginNode(String key);\n\n    /** Return true if the merge rules indicate that this key should be preserved. */\n    protected abstract boolean mergePreserveKey(T type, String key);\n\n    /** Return the props to use when a copy mod is applied with a path of {@code \"*\"}. */\n    protected abstract List<String> getCopyEntryProps();\n\n    /**\n     * Handle dynamic variables embedded within copy mods info.\n     *\n     * @param value JsonNode to be checked for values to replace\n     * @param target JsonNode with attributes that can be used to resolve templates\n     * @param variableMode The particular operation to use when resolving variables\n     * @param params Parameters for the variable mode\n     */\n    protected abstract JsonNode resolveDynamicVariable(\n            String originKey, JsonNode value, JsonNode target, TemplateVariable variableMode, String[] params);\n\n    /** Merge {@code copyFrom} into {@code target} according to copy metadata. */\n    protected abstract JsonNode mergeNodes(T type, String originKey, JsonNode copyFrom, ObjectNode target);\n\n    /** Handle any {@code _copy} fields which are present in the given node. This is the main entry point. */\n    public JsonNode handleCopy(T type, JsonNode copyTo) {\n        String copyToKey = type.createKey(copyTo);\n        JsonNode _copy = MetaFields._copy.getFrom(copyTo);\n        if (_copy != null) {\n            String copyFromKey = type.createKey(_copy);\n            JsonNode copyFrom = getOriginNode(copyFromKey);\n            if (copyToKey.equals(copyFromKey)) {\n                tui().errorf(\"(%s) Self-referencing copy. This is a data entry error.\", copyToKey, _copy);\n                return copyTo;\n            }\n            if (copyFrom == null) {\n                tui().errorf(\"(%s): Unable to find source %s to copy from\", copyToKey, copyFromKey);\n                return copyTo;\n            }\n            // is the copy a copy?\n            copyFrom = handleCopy(type, copyFrom);\n            try {\n                // edit in place: if you don't, lower-level copies will keep being revisted.\n                ObjectNode target = (ObjectNode) copyTo;\n\n                copyTo = mergeNodes(type, copyToKey, copyFrom, target);\n            } catch (JsonCopyException | StackOverflowError | UnsupportedOperationException e) {\n                tui().errorf(e, \"Error (%s): Unable to merge nodes. CopyTo: %s, CopyFrom: %s\", copyToKey, copyTo, copyFrom);\n            }\n        }\n        return copyTo;\n    }\n\n    /**\n     * Actually do the copy, copying required values from {@code copyFrom} into {@code copyTo}.\n     *\n     * @param _copy Node containing metadata about the copy\n     */\n    protected void copyValues(T type, JsonNode copyFrom, ObjectNode copyTo, JsonNode _copy) {\n        JsonNode _preserve = MetaFields._preserve.getFrom(_copy); // null is ok\n        // Copy required values from...\n        for (Entry<String, JsonNode> from : iterableFields(copyFrom)) {\n            String k = from.getKey();\n            JsonNode copyToField = copyTo.get(k);\n            if (copyToField != null && copyToField.isNull()) {\n                // copyToField is present / exists as an intentional `null`. Remove the field.\n                copyTo.remove(k);\n                continue;\n            }\n            if (copyToField == null) { // undefined\n                // not already present in copyTo -- should we copyFrom?\n                // Do merge rules indicate the value should be preserved\n                if (mergePreserveKey(type, k)) {\n                    // Does metadata indicate that it should be copied?\n                    if (metaPreserveKey(_preserve, k)) {\n                        copyTo.set(k, copyNode(from.getValue()));\n                    }\n                } else {\n                    // in general, yes.\n                    copyTo.set(k, copyNode(from.getValue()));\n                }\n            }\n        }\n    }\n\n    /**\n     * Apply modifiers to the {@code target}.\n     *\n     * @param originKey The key used to retrieve the target\n     * @param copyFrom The node that is being copied from\n     * @param target The target that the modifiers apply to\n     * @param _copy Metadata about the copy that contains mod data\n     */\n    protected void applyMods(String originKey, JsonNode copyFrom, ObjectNode target, JsonNode _copy) {\n        if (!MetaFields._mod.existsIn(_copy)) {\n            return;\n        }\n        // pre-convert any dynamic text\n        JsonNode copyMetaMod = MetaFields._mod.getFrom(_copy);\n        for (Entry<String, JsonNode> entry : iterableFields(copyMetaMod)) {\n            // use the target value as the attribute source for resolving dynamic text\n            entry.setValue(resolveDynamicText(originKey, entry.getValue(), target));\n        }\n\n        // Now iterate and apply mod rules\n        for (Entry<String, JsonNode> entry : iterableFields(copyMetaMod)) {\n            String prop = entry.getKey();\n            JsonNode modInfos = entry.getValue();\n            if (\"*\".equals(prop)) {\n                doMod(originKey, target, copyFrom, modInfos, getCopyEntryProps());\n            } else if (\"_\".equals(prop)) {\n                doMod(originKey, target, copyFrom, modInfos, null);\n            } else {\n                doMod(originKey, target, copyFrom, modInfos, List.of(prop));\n            }\n        }\n    }\n\n    private void doMod(String originKey, ObjectNode target, JsonNode copyFrom, JsonNode modInfos, List<String> props) {\n        if (props == null || props.isEmpty()) { // '_' case\n            doModProp(originKey, modInfos, copyFrom, null, target);\n        } else {\n            for (String prop : props) {\n                doModProp(originKey, modInfos, copyFrom, prop, target);\n            }\n        }\n    }\n\n    /** Apply a specific mod property. Print an error if the mod mode is not known. */\n    protected void doModProp(\n            String originKey, JsonNode modInfo, JsonNode copyFrom, String prop, ObjectNode target, ModFieldMode mode) {\n        switch (mode) {\n            // Strings & text\n            case appendStr -> doAppendText(originKey, modInfo, copyFrom, prop, target);\n            case replaceTxt -> doReplaceText(originKey, modInfo, copyFrom, prop, target);\n            // Properties\n            case setProp -> doSetProp(originKey, modInfo, prop, target);\n            case setProps -> doSetProps(originKey, modInfo, prop, target);\n            case prefixSuffixStringProp -> doPrefixSuffixStringProp(originKey, modInfo, prop, target);\n            // Arrays\n            case prependArr, appendArr, renameArr, replaceArr, replaceOrAppendArr, appendIfNotExistsArr, insertArr, removeArr ->\n                doModArray(originKey, mode, modInfo, prop, target);\n            // MATH\n            case scalarAddProp -> doScalarAddProp(originKey, modInfo, prop, target);\n            case scalarMultProp -> doScalarMultProp(originKey, modInfo, prop, target);\n            default -> tui().warnf(Msg.UNKNOWN, \"(%s): Unknown modification mode: %s\",\n                    originKey, modInfo);\n        }\n    }\n\n    protected void doModProp(String originKey, JsonNode modInfos, JsonNode copyFrom, String prop, ObjectNode target) {\n        for (JsonNode modInfo : iterableElements(modInfos)) {\n            if (modInfo.isTextual()) {\n                if (\"remove\".equals(modInfo.asText()) && prop != null) {\n                    target.remove(prop);\n                } else {\n                    tui().warnf(Msg.UNKNOWN, \"(%s): Unknown text modification mode for %s: %s\", originKey, prop, modInfo);\n                }\n            } else {\n                doModProp(originKey, modInfo, copyFrom, prop, target, ModFieldMode.getModMode(modInfo));\n            }\n        }\n    }\n\n    private static boolean metaPreserveKey(JsonNode _preserve, String key) {\n        return _preserve != null && (_preserve.has(\"*\") || _preserve.has(key));\n    }\n\n    protected void normalizeMods(JsonNode copyMeta) {\n        if (MetaFields._mod.existsIn(copyMeta)) {\n            ObjectNode mods = (ObjectNode) MetaFields._mod.getFrom(copyMeta);\n            for (String name : iterableFieldNames(mods)) {\n                JsonNode mod = mods.get(name);\n                if (!mod.isArray()) {\n                    mods.set(name, mapper().createArrayNode().add(mod));\n                }\n            }\n        }\n    }\n\n    /** Indicate that the given node is a copy, and remove copy metadata to avoid revisit. */\n    protected void cleanupCopy(ObjectNode target, JsonNode copyFrom) {\n        MetaFields._isCopy.setIn(target, true);\n        MetaFields._rawName.removeFrom(target);\n        MetaFields._copiedFrom.setIn(target, String.format(\"%s (%s)\",\n                SourceField.name.getTextOrEmpty(copyFrom),\n                SourceField.source.getTextOrEmpty(copyFrom)));\n        MetaFields._copy.removeFrom(target);\n    }\n\n    // DataUtil.generic.variableResolver\n    /**\n     * @param originKey The origin key used to get the target from the index\n     * @param value JsonNode to be checked for values to replace\n     * @param target JsonNode with attributes that can be used to resolve templates\n     */\n    protected JsonNode resolveDynamicText(String originKey, JsonNode value, JsonNode target) {\n        if (value == null || !(value.isArray() || value.isObject() || value.isTextual())) {\n            return value;\n        }\n        if (value.isArray()) {\n            for (int i = 0; i < value.size(); i++) {\n                ((ArrayNode) value).set(i, resolveDynamicText(originKey, value.get(i), target));\n            }\n            return value;\n        }\n        if (value.isObject()) {\n            for (Entry<String, JsonNode> e : iterableFields(value)) {\n                e.setValue(resolveDynamicText(originKey, e.getValue(), target));\n            }\n            return value;\n        }\n        Matcher matcher = VARIABLE_SUBST_PAT.matcher(value.toString());\n        if (matcher.find()) {\n            String[] params = matcher.group(\"variable\").split(\"__\");\n            TemplateVariable variableMode = TemplateVariable.valueFrom(params[0]);\n            return resolveDynamicVariable(\n                    originKey, value, target, variableMode, Arrays.copyOfRange(params, 1, params.length));\n        }\n        return value;\n    }\n\n    /** Set the target prop which corresponds to the prop in {@code modInfo} to the value from {@code modInfo}. */\n    private void doSetProp(String originKey, JsonNode modInfo, String prop, ObjectNode target) {\n        // target.(prop . modinfo.prop) = modinfo.value\n        String propPath = MetaFields.prop.getTextOrEmpty(modInfo);\n        if (!\"*\".equals(prop)) {\n            // target.(prop . modinfo.prop) = modinfo.value\n            propPath = prop + \".\" + propPath;\n        }\n        String[] path = splitLastPropPath(propPath);\n        ObjectNode targetRw = target.withObject(path[0]);\n        targetRw.set(path[1], copyNode(MetaFields.value.getFrom(modInfo)));\n    }\n\n    /** Set the {@code propPath} in {@code target} to contain the props in {@code modInfo}. */\n    private void doSetProps(String originKey, JsonNode modInfo, String propPath, ObjectNode target) {\n        String[] path = splitLastPropPath(propPath);\n        ObjectNode parent = propPath.equals(\"*\") ? target : target.withObject(path[0]);\n        JsonNode propNode = copyNode(MetaFields.props.getFrom(modInfo));\n        if (propNode.isObject()) {\n            parent.setAll((ObjectNode) propNode);\n        } else {\n            parent.set(path[1], propNode);\n        }\n    }\n\n    /** Set the target prop which corresponds to the prop in {@code modInfo} to the value from {@code modInfo}. */\n    private void doPrefixSuffixStringProp(String originKey, JsonNode modInfo, String prop, ObjectNode target) {\n        // target.(prop . modinfo.prop) = modinfo.value\n        String propPath = MetaFields.prop.getTextOrEmpty(modInfo);\n        if (!\"*\".equals(prop)) {\n            // target.(prop . modinfo.prop) = modinfo.value\n            propPath = prop + \".\" + propPath;\n        }\n        String prefix = MetaFields.prefix.getTextOrEmpty(modInfo);\n        String suffix = MetaFields.suffix.getTextOrEmpty(modInfo);\n\n        String[] path = splitLastPropPath(propPath);\n        ObjectNode targetRw = target.withObject(path[0]);\n\n        // Verify that we're going to replace a string value\n        JsonNode targetValue = targetRw.get(path[1]);\n        if (targetValue == null || !targetValue.isTextual()) {\n            return;\n        }\n        // Update the string value, add prefix and suffix\n        targetRw.put(path[1],\n                prefix\n                        + MetaFields.value.getTextOrEmpty(modInfo)\n                        + suffix);\n    }\n\n    private String nodePath(String propPath) {\n        return \"/\" + String.join(\"/\", propPath.split(\"\\\\.\"));\n    }\n\n    private String[] splitLastPropPath(String propPath) {\n        String nodePath = nodePath(propPath);\n        int lastPropIdx = nodePath.lastIndexOf('/');\n        return new String[] { nodePath.substring(0, lastPropIdx), nodePath.substring(lastPropIdx) };\n    }\n\n    private void doAppendText(String originKey, JsonNode modInfo, JsonNode copyFrom, String prop, ObjectNode target) {\n        if (target.has(prop)) {\n            String joiner = MetaFields.joiner.getTextOrEmpty(modInfo);\n            target.put(prop, target.get(prop).asText() + joiner\n                    + MetaFields.str.getTextOrEmpty(modInfo));\n        } else {\n            target.put(prop, MetaFields.str.getTextOrEmpty(modInfo));\n        }\n    }\n\n    private void doReplaceText(String originKey, JsonNode modInfo, JsonNode copyFrom, String prop, ObjectNode target) {\n        if (!target.has(prop)) {\n            return;\n        }\n        if (!target.get(prop).isArray()) {\n            tui().warnf(\"replaceTxt for %s with a property %s that is not an array %s: %s\", originKey, prop, modInfo,\n                    target.get(prop));\n            return;\n        }\n\n        String replace = MetaFields.replace.getTextOrEmpty(modInfo);\n        String with = MetaFields.with.getTextOrEmpty(modInfo);\n        JsonNode flags = MetaFields.flags.getFrom(modInfo);\n\n        final Pattern pattern;\n        if (flags != null) {\n            int pFlags = 0;\n            if (flags.asText().contains(\"i\")) {\n                pFlags |= Pattern.CASE_INSENSITIVE;\n            }\n            pattern = Pattern.compile(\"\\\\b\" + replace, pFlags);\n        } else {\n            pattern = Pattern.compile(\"\\\\b\" + replace);\n        }\n\n        final boolean findPlainText;\n        final List<String> propNames;\n        JsonNode props = MetaFields.props.getFrom(modInfo);\n        if (props == null) {\n            findPlainText = true;\n            propNames = List.of(\"entries\", \"headerEntries\", \"footerEntries\");\n        } else if (props.isEmpty()) {\n            tui().warnf(\"replaceText with empty props in %s: %s\", originKey, modInfo);\n            return;\n        } else {\n            propNames = new ArrayList<>();\n            props.forEach(x -> propNames.add(x.isNull() ? \"null\" : x.asText()));\n            findPlainText = propNames.remove(\"null\");\n        }\n\n        ArrayNode tgtArray = target.withArray(prop);\n        for (int i = 0; i < tgtArray.size(); i++) {\n            JsonNode it = tgtArray.get(i);\n            if (it.isTextual() && findPlainText) {\n                tgtArray.set(i, copyReplaceText(it, pattern, with));\n            } else if (it.isObject()) {\n                for (String k : propNames) {\n                    if (it.has(k)) {\n                        ((ObjectNode) it).set(k, copyReplaceText(it.get(k), pattern, with));\n                    }\n                }\n            }\n        }\n    }\n\n    private JsonNode copyReplaceText(JsonNode sourceNode, Pattern replace, String with) {\n        String modified = replace.matcher(sourceNode.toString()).replaceAll(with);\n        return createNode(modified);\n    }\n\n    private void doScalarAddProp(String originKey, JsonNode modInfo, String prop, ObjectNode target) {\n        if (!target.has(prop)) {\n            return;\n        }\n        ObjectNode propRw = (ObjectNode) target.get(prop);\n        int scalar = MetaFields.scalar.getFrom(modInfo).asInt();\n        Consumer<String> scalarAdd = (k) -> {\n            JsonNode node = propRw.get(k);\n            boolean isString = node.isTextual();\n            int value = isString\n                    ? Integer.parseInt(node.asText())\n                    : node.asInt();\n            value += scalar;\n            propRw.replace(k, isString\n                    ? new TextNode(\"%+d\".formatted(value))\n                    : new IntNode(value));\n        };\n\n        String modProp = MetaFields.prop.getTextOrNull(modInfo);\n        if (\"*\".equals(modProp)) {\n            for (String fieldName : iterableFieldNames(propRw)) {\n                scalarAdd.accept(fieldName);\n            }\n        } else {\n            scalarAdd.accept(modProp);\n        }\n    }\n\n    private void doScalarMultProp(String originKey, JsonNode modInfo, String prop, ObjectNode target) {\n        if (!target.has(prop)) {\n            return;\n        }\n        ObjectNode propRw = (ObjectNode) target.get(prop);\n        double scalar = MetaFields.scalar.getFrom(modInfo).asDouble();\n        boolean floor = MetaFields.floor.booleanOrDefault(modInfo, false);\n        Consumer<String> scalarMult = (k) -> {\n            JsonNode node = propRw.get(k);\n            boolean isString = node.isTextual();\n            double value = isString\n                    ? Double.parseDouble(node.asText())\n                    : node.asDouble();\n            value *= scalar;\n            if (floor) {\n                value = Math.floor(value);\n            }\n            propRw.replace(k, isString\n                    ? new TextNode(\"%+f\".formatted(value))\n                    : new DoubleNode(value));\n        };\n\n        String modProp = MetaFields.prop.getTextOrNull(modInfo);\n        if (\"*\".equals(modProp)) {\n            for (String fieldName : iterableFieldNames(propRw)) {\n                scalarMult.accept(fieldName);\n            }\n        } else {\n            scalarMult.accept(modProp);\n        }\n    }\n\n    private void doModArray(String originKey, ModFieldMode mode, JsonNode modInfo, String prop, ObjectNode target) {\n        JsonNode items = ensureArray(MetaFields.items.getFrom(modInfo));\n        String propPath = nodePath(prop);\n\n        switch (mode) {\n            case insertArr, removeArr, renameArr, replaceArr -> {\n                if (target.at(propPath).isMissingNode()) {\n                    tui().errorf(\"Error (%s): Unable to %s; %s is not present: %s\", originKey, mode, prop, target);\n                    return;\n                }\n            }\n            default -> {\n            }\n        }\n\n        ArrayNode targetArray = target.withArray(propPath);\n        switch (mode) {\n            case prependArr -> insertIntoArray(targetArray, 0, items);\n            case appendArr -> appendToArray(targetArray, items);\n            case appendIfNotExistsArr -> appendIfNotExistsArr(targetArray, items);\n            case insertArr -> insertIntoArray(\n                    targetArray,\n                    MetaFields.index.intFrom(modInfo).filter(n -> n >= 0).orElse(targetArray.size()),\n                    items);\n            case removeArr -> removeFromArray(originKey, modInfo, prop, targetArray);\n            case renameArr -> renameInArray(originKey, modInfo, targetArray);\n            case replaceArr -> replaceArray(originKey, modInfo, targetArray, items);\n            case replaceOrAppendArr -> {\n                boolean didReplace = !targetArray.isEmpty() && replaceArray(originKey, modInfo, targetArray, items);\n                if (!didReplace) {\n                    appendToArray(targetArray, items);\n                }\n            }\n            default -> tui().warnf(Msg.UNKNOWN, \"(%s): Unknown modification mode for property %s: %s\",\n                    originKey, prop, modInfo);\n        }\n    }\n\n    protected ArrayNode sortArrayNode(ArrayNode array) {\n        if (array == null || array.size() <= 1) {\n            return array;\n        }\n        Set<JsonNode> elements = new TreeSet<>(Comparator.comparing(a -> a.asText().toLowerCase()));\n        array.forEach(elements::add);\n        ArrayNode sorted = mapper().createArrayNode();\n        sorted.addAll(elements);\n        return sorted;\n    }\n\n    protected void appendToArray(ArrayNode tgtArray, JsonNode items) {\n        if (items == null) {\n            return;\n        }\n        if (items.isArray()) {\n            tgtArray.addAll((ArrayNode) items);\n        } else {\n            tgtArray.add(items);\n        }\n    }\n\n    protected void insertIntoArray(ArrayNode tgtArray, int index, JsonNode items) {\n        if (items == null) {\n            return;\n        }\n        if (items.isArray()) {\n            // iterate backwards so that items end up in the right order @ desired index\n            for (int i = items.size() - 1; i >= 0; i--) {\n                tgtArray.insert(index, items.get(i));\n            }\n        } else {\n            tgtArray.insert(index, items);\n        }\n    }\n\n    public void appendIfNotExistsArr(ArrayNode tgtArray, JsonNode items) {\n        if (items == null) {\n            return;\n        }\n        if (tgtArray.size() == 0) {\n            appendToArray(tgtArray, items);\n        } else {\n            // Remove inbound items that already exist in the target array\n            // Use anyMatch to stop filtering ASAP\n            List<JsonNode> filtered = streamOf(items)\n                    .filter(it -> !streamOf(tgtArray).anyMatch(it::equals))\n                    .collect(Collectors.toList());\n            tgtArray.addAll(filtered);\n        }\n    }\n\n    protected void removeFromArray(String originKey, JsonNode modInfo, String prop, ArrayNode tgtArray) {\n        JsonNode names = ensureArray(MetaFields.names.getFrom(modInfo));\n        JsonNode items = ensureArray(MetaFields.items.getFrom(modInfo));\n        if (names != null) {\n            for (JsonNode name : iterableElements(names)) {\n                int index = findIndexByName(originKey, tgtArray, name.asText());\n                if (index >= 0) {\n                    tgtArray.remove(index);\n                } else if (!MetaFields.force.booleanOrDefault(modInfo, false)) {\n                    tui().errorf(\"Error (%s / %s): Unable to remove %s; %s\", originKey, prop, name.asText(), modInfo);\n                }\n            }\n        } else if (items != null) {\n            removeFromArr(tgtArray, items);\n        } else {\n            tui().errorf(\"Error (%s / %s): One of names or items must be provided to remove elements from array; %s\", originKey,\n                    prop, modInfo);\n        }\n    }\n\n    public void removeFromArr(ArrayNode tgtArray, JsonNode items) {\n        for (JsonNode itemToRemove : iterableElements(items)) {\n            int index = findIndex(tgtArray, itemToRemove);\n            if (index >= 0) {\n                tgtArray.remove(index);\n            }\n        }\n    }\n\n    protected void renameInArray(String originKey, JsonNode modInfo, ArrayNode tgtArray) {\n        JsonNode renames = ensureArray(MetaFields.renames.getFrom(modInfo));\n        if (renames == null || !renames.isArray()) {\n            return;\n        }\n\n        for (JsonNode renameNode : iterableElements(renames)) {\n            int index = findIndexByName(originKey, tgtArray, MetaFields.rename.getTextOrEmpty(renameNode));\n            if (index >= 0) {\n                JsonNode element = tgtArray.get(index);\n                SourceField.name.setIn(element, MetaFields.with.getFrom(renameNode));\n            }\n        }\n    }\n\n    protected boolean replaceArray(String originKey, JsonNode modInfo, ArrayNode tgtArray, JsonNode items) {\n        if (items == null || !items.isArray()) {\n            return false;\n        }\n        JsonNode replace = MetaFields.replace.getFrom(modInfo);\n\n        final int index;\n        if (replace.isTextual()) {\n            index = findIndexByName(originKey, tgtArray, replace.asText());\n        } else if (replace.isObject() && MetaFields.index.existsIn(replace)) {\n            index = MetaFields.index.intOrDefault(replace, 0);\n        } else if (replace.isObject() && MetaFields.regex.existsIn(replace)) {\n            Pattern pattern = Pattern.compile(\"\\\\b\" + MetaFields.regex.getTextOrEmpty(replace));\n            index = matchFirstIndexByName(originKey, tgtArray, pattern);\n        } else {\n            tui().warnf(Msg.UNKNOWN, \"(%s): Unknown replace; %s\", originKey, modInfo);\n            return false;\n        }\n\n        if (index >= 0) {\n            tgtArray.remove(index);\n            insertIntoArray(tgtArray, index, items);\n            return true;\n        }\n        return false;\n    }\n\n    private int matchFirstIndexByName(String originKey, ArrayNode haystack, Pattern needle) {\n        for (int i = 0; i < haystack.size(); i++) {\n            final String toMatch;\n            if (haystack.get(i).isObject()) {\n                toMatch = SourceField.name.getTextOrEmpty(haystack.get(i));\n            } else if (haystack.get(i).isTextual()) {\n                toMatch = haystack.asText();\n            } else {\n                continue;\n            }\n            if (!toMatch.isBlank() && needle.matcher(toMatch).find()) {\n                return i;\n            }\n        }\n        return -1;\n    }\n\n    protected int findIndexByName(String originKey, ArrayNode haystack, String needle) {\n        for (int i = 0; i < haystack.size(); i++) {\n            final String toMatch;\n            if (haystack.get(i).isObject()) {\n                toMatch = SourceField.name.getTextOrEmpty(haystack.get(i));\n            } else if (haystack.get(i).isTextual()) {\n                toMatch = haystack.get(i).asText();\n            } else {\n                continue;\n            }\n            if (needle.equals(toMatch)) {\n                return i;\n            }\n        }\n        return -1;\n    }\n\n    private int findIndex(ArrayNode haystack, JsonNode needle) {\n        for (int i = 0; i < haystack.size(); i++) {\n            if (haystack.get(i).equals(needle)) {\n                return i;\n            }\n        }\n        return -1;\n    }\n\n    public enum MetaFields implements JsonNodeReader {\n        _copy,\n        _copiedFrom, // mind\n        _isCopy,\n        _mod,\n        _preserve,\n        _rawName,\n        _root,\n        _templates,\n        alias,\n        apply,\n        data,\n        dex,\n        dex_mod,\n        flags,\n        floor,\n        force,\n        index,\n        items,\n        joiner,\n        max,\n        mode,\n        names,\n        overwrite,\n        prefix,\n        prof_bonus,\n        prop,\n        props,\n        range,\n        regex,\n        rename,\n        renames,\n        replace,\n        root,\n        scalar,\n        skills,\n        str,\n        suffix,\n        type,\n        value,\n        with,\n        ;\n    }\n\n    public enum TemplateVariable implements JsonNodeReader.FieldValue {\n        name,\n        short_name,\n        title_short_name,\n        dc,\n        spell_dc,\n        to_hit,\n        damage_mod,\n        damage_avg;\n\n        public void notSupported(Tui tui, String originKey, JsonNode variableText) {\n            tui.errorf(\"Error (%s): Support for %s must be implemented. Raise an issue with this message. Text: %s\",\n                    originKey, this.value(), variableText);\n        }\n\n        public static TemplateVariable valueFrom(String value) {\n            return FieldValue.valueFrom(value, TemplateVariable.class);\n        }\n    }\n\n    public enum ModFieldMode implements JsonNodeReader.FieldValue {\n        appendStr,\n        replaceName,\n        replaceTxt,\n\n        prependArr,\n        appendArr,\n        renameArr,\n        replaceArr,\n        replaceOrAppendArr,\n        appendIfNotExistsArr,\n        insertArr,\n        removeArr,\n\n        calculateProp,\n        scalarAddProp,\n        scalarMultProp,\n        setProp,\n        setProps,\n        prefixSuffixStringProp,\n\n        addSenses,\n        addSaves,\n        addSkills,\n        addAllSaves,\n        addAllSkills,\n\n        addSpells,\n        removeSpells,\n        replaceSpells,\n\n        maxSize,\n        scalarMultXp,\n        scalarAddHit,\n        scalarAddDc;\n\n        public void notSupported(Tui tui, String originKey, JsonNode modInfo) {\n            tui.errorf(\"Error (%s): %s must be implemented. Raise an issue with this message. modInfo: %s\",\n                    originKey, this.value(), modInfo);\n        }\n\n        public static ModFieldMode getModMode(JsonNode source) {\n            return FieldValue.valueFrom(MetaFields.mode.getTextOrNull(source), ModFieldMode.class);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/JsonTextConverter.java",
    "content": "package dev.ebullient.convert.tools;\n\nimport static dev.ebullient.convert.StringUtil.isPresent;\nimport static dev.ebullient.convert.StringUtil.join;\nimport static dev.ebullient.convert.StringUtil.toAnchorTag;\nimport static dev.ebullient.convert.StringUtil.valueOrDefault;\n\nimport java.nio.file.Path;\nimport java.util.ArrayDeque;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Map.Entry;\nimport java.util.function.BiFunction;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport java.util.stream.IntStream;\nimport java.util.stream.Stream;\nimport java.util.stream.StreamSupport;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.node.ArrayNode;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\n\nimport dev.ebullient.convert.config.CompendiumConfig;\nimport dev.ebullient.convert.io.Msg;\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.qute.QuteBase;\nimport dev.ebullient.convert.qute.QuteUtil;\nimport dev.ebullient.convert.tools.ParseState.DiceFormulaState;\n\npublic interface JsonTextConverter<T extends IndexType> {\n    public static String DICE_FORMULA = \"[ +d\\\\d-]+\";\n    public static String DICE_TABLE_HEADER = \"\\\\| dice: \\\\d*d\\\\d+ \\\\|.*\";\n\n    static final Pattern dicePattern = Pattern.compile(\"\\\\{@(\"\n            + \"dice|autodice|damage|h |hit|d20|initiative|scaledice|scaledamage\"\n            + \") ?([^}]+)}\");\n    static final Pattern dicePatternWithSpan = Pattern.compile(\"(.+)(<span[^>]+>)(.+)(</span>)\");\n    static final Pattern footnotePattern = Pattern.compile(\"\\\\{@footnote ([^}]+)}\");\n    static final Pattern textAverageRoll = Pattern.compile(\" (\\\\d+) \\\\((`dice:[^`]+text\\\\(([^)]+)\\\\)`)\\\\)\");\n    static final Pattern averageRoll = Pattern.compile(\" (\\\\d+) \\\\(`(dice:[^`]+)` (\\\\([^)]+\\\\))\\\\)\");\n\n    void appendToText(List<String> inner, JsonNode target, String heading);\n\n    Tui tui();\n\n    CompendiumConfig cfg();\n\n    default ObjectMapper mapper() {\n        return Tui.MAPPER;\n    }\n\n    default ParseState parseState() {\n        return cfg().parseState();\n    }\n\n    default JsonNode copyNode(JsonNode sourceNode) {\n        try {\n            return mapper().readTree(sourceNode.toString());\n        } catch (JsonProcessingException ex) {\n            tui().errorf(ex, \"Unable to copy %s\", sourceNode.toString());\n            System.exit(5);\n            return null;\n        }\n    }\n\n    default JsonNode createNode(String source) {\n        try {\n            return mapper().readTree(source);\n        } catch (JsonProcessingException ex) {\n            tui().errorf(ex, \"Unable to create node from %s\", source);\n            System.exit(5);\n            return null;\n        }\n    }\n\n    default boolean isEmpty(JsonNode node) {\n        return node == null || node.isNull()\n                || (node.isTextual() && node.asText().isBlank()\n                        || (node.isArray() && node.size() == 0)\n                        || (node.isObject() && node.size() == 0));\n    }\n\n    default boolean isArrayNode(JsonNode node) {\n        return node != null && node.isArray();\n    }\n\n    default boolean isObjectNode(JsonNode node) {\n        return node != null && node.isObject();\n    }\n\n    default JsonNode objectIntersect(JsonNode a, JsonNode b) {\n        if (a.equals(b)) {\n            return a;\n        }\n        ObjectNode x = Tui.MAPPER.createObjectNode();\n        for (String k : iterableFieldNames(a)) {\n            if (a.get(k).equals(b.get(k))) {\n                x.set(k, a.get(k));\n            } else if (isObjectNode(a.get(k)) && isObjectNode(b.get(k))) {\n                x.set(k, objectIntersect(a.get(k), b.get(k)));\n            }\n        }\n        return x;\n    }\n\n    default String replaceWithDiceRoller(String input) {\n        if (!isPresent(input)) {\n            return input;\n        }\n        if (parseState().inHtmlTable()) {\n            // Dice roller syntax doesn't work in HTML tables;\n            // strip dice tags to plain text (display text or roll string)\n            return input\n                    .replaceAll(\"\\\\{@d20}\", \"d20\")\n                    .replaceAll(\n                            \"\\\\{@(?:dice|damage|autodice|hit|d20|initiative|scaledice|scaledamage) ([^}|]+)\\\\|([^|}]+)[^}]*}\",\n                            \"$2\")\n                    .replaceAll(\"\\\\{@(?:dice|damage|autodice|hit|d20|initiative|scaledice|scaledamage) ([^}|]+)}\", \"$1\")\n                    .replaceAll(\"\\\\{@hitYourSpellAttack ([^}]+)}\", \"$1\")\n                    .replaceAll(\"\\\\{@hitYourSpellAttack}\", \"your spell attack modifier\");\n        }\n        if (input.equals(\"{@d20}\")) {\n            // this is a weird case where the input is just a d20 roll\n            input = \"{@dice d20}\";\n        }\n\n        DiceFormulaState formulaState = parseState().diceFormulaState();\n\n        input = input.replaceAll(\"\\\\{@hitYourSpellAttack ([^}]+)}\", \"$1\")\n                .replaceAll(\"\\\\{@hitYourSpellAttack}\", \"your spell attack modifier\");\n\n        Matcher m = dicePattern.matcher(input);\n        if (!m.find()) {\n            return input;\n        }\n        if (m.groupCount() < 2) {\n            tui().warnf(Msg.UNKNOWN, \"Unknown/Invalid dice formula Input: %s\", input);\n            return input;\n        }\n\n        String tag = m.group(1);\n        String[] parts = m.group(2).split(\"\\\\|\");\n\n        String rollString = parts[0].trim();\n        String displayText = valueOrDefault(parts, 1, null);\n        String scaleSkillName = valueOrDefault(parts, 2, null);\n\n        return switch (tag) {\n            case \"d20\", \"h\", \"hit\", \"initiative\" -> {\n                // {@d20 -4}, {@d20 -2 + PB},\n                // {@d20 0|10}, {@d20 2|+2|Perception}, {@d20 -1|\\u22121|Father Belderone}\n                // {@hit +7}, {@hit 6|+6|Slam}, {@hit 6|+6 bonus}, {@hit +3|+3 to hit}\n                // @initiative -- like @hit\n                String posGroup = \"(?<!-)\\\\+? ?(\\\\d+)\";\n                String mod = rollString.replaceAll(posGroup, \"+$1\");\n                String mod20 = \"1d20\" + mod;\n\n                if (scaleSkillName != null) {\n                    //  Perception (`+2`), Father Belderone (`-1`), Slam (`+6`)\n                    yield scaleSkillName + \" (\"\n                            + formatDice(mod20, codeString(mod, formulaState), formulaState, false, false)\n                            + \")\";\n                }\n\n                String formattedRoll = formatDice(mod20,\n                        codeString(mod, formulaState),\n                        formulaState, false, false);\n\n                if (displayText != null) {\n                    if (tag.equals(\"hit\")) {\n                        // +6 bonus, +3 to hit\n                        yield displayText;\n                    }\n                    // non-standard order: `+0` (`10`)\n                    yield formattedRoll + \" (\" + codeString(displayText, formulaState) + \")\";\n                }\n                yield formattedRoll;\n            }\n            case \"scaledamage\", \"scaledice\" -> {\n                // damage of 2d6 or 3d6 at level 1: {@scaledamage 2d6;3d6|2-9|1d6} for each level beyond 2nd;\n                // roll 2d6 when using 1 psi point: {@scaledice 2d6|1,3,5,7,9|1d6|psi|extra amount} for each additional psi point spent\n                // format: {@scaledice 2d6;3d6|2-8,9|1d6|psi|display text} (or @scaledamage)\n                // [baseRoll, progression, addPerProgress, renderMode, displayText]\n                yield parts.length > 4\n                        ? formatDice(scaleSkillName, parts[4].trim(), formulaState, true, true)\n                        : formatDice(scaleSkillName, codeString(scaleSkillName, formulaState), formulaState, true, false);\n            }\n            // {@dice d6}\n            // {@dice d20}\n            // {@dice 1d2-2+2d3+5} for regular dice rolls\n            // {@dice 1d6;2d6} for multiple options;\n            // {@dice 1d6 + #$prompt_number:min=1,title=Enter a Number!,default=123$#} for input prompts\n            // --> prompts will have been replaced with default value: {@dice 1d6 + <span...>lotsofstuff</span>}\n            // {@dice 1d20+2|display text}\n            // {@dice 1d20+2|display text|rolled by name}\n            // {@damage 1d12+3}\n            // @autodice -- like @dice\n            default -> {\n                String[] alternatives = rollString.split(\";\");\n                if (displayText == null && alternatives.length > 1) {\n                    for (int i = 0; i < alternatives.length; i++) {\n                        String coded = codeString(alternatives[i], formulaState);\n                        alternatives[i] = formatDice(alternatives[i], coded, formulaState, true, false);\n                    }\n                    displayText = String.join(\" or \", alternatives);\n                }\n                yield formatDice(rollString, displayText, formulaState, true, true);\n            }\n        };\n    }\n\n    default String formatDice(String rollString, String displayText, DiceFormulaState formulaState, boolean useAverage,\n            boolean appendFormula) {\n        if (rollString.contains(\";\")) {\n            return displayText;\n        }\n        if (rollString.contains(\"<span\")) {\n            // There is (or was) an input prompt here.\n            Matcher m = dicePatternWithSpan.matcher(rollString);\n            if (m.matches()) {\n                displayText = \"%s%s%s\".formatted(m.group(2), codeString(m.group(1) + m.group(3), formulaState), m.group(4));\n            }\n        }\n        if (displayText == null && rollString.contains(\"summonSpellLevel\")) {\n            displayText = codeString(rollString.replace(\" + summonSpellLevel\", \"\"), formulaState)\n                    + \" + the spell's level\";\n        } else if (displayText != null && displayText.contains(\"summonSpellLevel\")) {\n            displayText = displayText.replace(\"summonSpellLevel\", \"the spell's level\");\n        }\n        if (displayText == null && rollString.contains(\"summonClassLevel\")) {\n            displayText = codeString(rollString.replace(\" + summonClassLevel\", \"\"), formulaState)\n                    + \" + your class level\";\n        } else if (displayText != null && displayText.contains(\"summonClassLevel\")) {\n            displayText = displayText.replace(\"summonClassLevel\", \"your class level\");\n        }\n\n        String dice = codeString(rollString, formulaState);\n\n        if (rollString.matches(JsonTextConverter.DICE_FORMULA)) {\n            String postText = appendFormula ? \" (\" + dice + \")\" : \"\";\n            if (formulaState.noRoller()) {\n                return displayText == null\n                        ? dice\n                        : displayText + postText;\n            }\n            return diceFormula(rollString.replace(\" \", \"\"), displayText, useAverage) + postText;\n        } else {\n            // Most likely have display text here. (Prompt, spell level, class level most likely cause)\n            return displayText == null\n                    ? dice\n                    : displayText;\n        }\n    }\n\n    default String diceFormula(String diceRoll) {\n        // Only a dice formula in the roll part. May also have display text.\n        return \"`dice:\" + diceRoll + \"`\";\n    }\n\n    default String diceFormula(String diceRoll, String displayText, boolean average) {\n        // don't escape the dice formula here.\n        // see simplifyFormattedDiceText (called consistently from replaceText)\n        String noform = \"|noform|noparens\";\n        String avg = \"|avg\";\n        String dtxt = \"|text(\";\n        String textValue = displayText == null ? \"\" : displayText.replace(\"`\", \"\");\n\n        // Only a dice formula in the roll part. May also have display text.\n        return \"`dice:\" + diceRoll + noform +\n                (average ? avg : \"\") +\n                (displayText == null ? \"`\" : dtxt + textValue + \")`\");\n    }\n\n    default String codeString(String text, DiceFormulaState formulaState) {\n        if (text.matches(\"^1?d\\\\d+$\")) {\n            return formulaState.plainText() ? text : \"`%s`\".formatted(text);\n        }\n        text = text.replace(\"1d20\", \"\");\n        return formulaState.plainText() ? text : \"`\" + text + \"`\";\n    }\n\n    // reduce dice strings.. when parsing tags, we can't see leadng average\n    default String simplifyFormattedDiceText(String text) {\n        DiceFormulaState formulaState = parseState().diceFormulaState();\n        String dtxt = \"|text(\";\n\n        // 26 (`dice:1d20+8|noform|text(+8)`) --> `dice:1d20+8|noform|text(26)` (`+8`)\n        text = textAverageRoll.matcher(text).replaceAll((match) -> {\n            String replaceText = \"(\" + match.group(3) + \")\";\n            String avgValue = \"(\" + match.group(1) + \")\";\n            return \" \" + match.group(2).replace(replaceText, avgValue)\n                    + \" (\" + codeString(match.group(3), formulaState) + \")\";\n        });\n\n        if (text.contains(\"reach levels\")) {\n            // don't look for averages here. This is spell progression\n        } else if (text.matches(\"^`dice:1?d\\\\d+\\\\|.*?` \\\\(`1?d\\\\d+`\\\\)\")) {\n            text = text.replaceAll(\"^`dice:(1)?d(\\\\d+)\\\\|.*\",\n                    \"`dice:1d$2|noform|noparens|avg|text($1d$2)`\");\n        } else {\n            // otherwise look for average rolls\n            // 7 (`dice:1d6+4|noform|avg` (`1d6 + 4`)) --> `dice:1d6+4|noform|avg|text(7)` (`1d6 + 4`)\n            // 7 (`dice:2d6|noform|avg` (`2d6`)) --> `dice:2d6|noform|avg|text(7)` (`2d6`)\n            text = averageRoll.matcher(text).replaceAll((match) -> {\n                String dice = match.group(2) + dtxt + match.group(1) + \")\";\n                return \" `\" + dice + \"` \" + match.group(3);\n            });\n        }\n\n        return parseState().inMarkdownTable()\n                ? text.replace(\"|\", \"\\\\|\")\n                : text;\n    }\n\n    /** Tokenizer: use a stack of StringBuilders to deal with nested tags */\n    default String replaceTokens(String input, BiFunction<String, Boolean, String> tokenResolver) {\n        if (input == null || input.isBlank()) {\n            return input;\n        }\n\n        StringBuilder out = new StringBuilder();\n        ArrayDeque<StringBuilder> stack = new ArrayDeque<>();\n        StringBuilder buffer = new StringBuilder();\n        boolean foundDice = false;\n\n        for (int i = 0; i < input.length(); i++) {\n            char c = input.charAt(i);\n\n            switch (c) {\n                case '{':\n                    stack.push(buffer);\n                    buffer = new StringBuilder();\n                    buffer.append(c);\n                    break;\n                case '}':\n                    buffer.append(c);\n                    String replace = tokenResolver.apply(buffer.toString(), stack.size() > 1);\n                    foundDice |= replace.contains(\"`dice:\");\n                    if (stack.isEmpty()) {\n                        tui().warnf(\"Mismatched braces? Found '}' with an empty stack. Input: %s\", input);\n                    } else {\n                        buffer = stack.pop();\n                    }\n                    buffer.append(replace);\n                    break;\n                default:\n                    buffer.append(c);\n                    break;\n            }\n        }\n\n        if (buffer.length() > 0) {\n            out.append(buffer);\n        }\n        return foundDice\n                ? simplifyFormattedDiceText(out.toString())\n                : out.toString();\n    }\n\n    default Iterable<JsonNode> iterableElements(JsonNode source) {\n        if (source == null) {\n            return List.of();\n        }\n        if (!source.isArray()) {\n            return List.of(source);\n        }\n        return source::elements;\n    }\n\n    default Iterable<JsonNode> iterableEntries(JsonNode source) {\n        JsonNode entries = source.get(\"entries\");\n        if (entries == null) {\n            return List.of();\n        }\n        return entries::elements;\n    }\n\n    default Iterable<Entry<String, JsonNode>> iterableFields(JsonNode source) {\n        if (source == null) {\n            return List.of();\n        }\n        return source.properties();\n    }\n\n    default Iterable<String> iterableFieldNames(JsonNode source) {\n        if (source == null) {\n            return List.of();\n        }\n        return source::fieldNames;\n    }\n\n    String linkify(T type, String s);\n\n    /** Internal / recursive parse */\n    default void appendListItem(List<String> text, JsonNode itemNode) {\n        List<String> inner = new ArrayList<>();\n        appendToText(inner, SourceField.entry.getFrom(itemNode), null);\n        appendToText(inner, SourceField.entries.getFrom(itemNode), null);\n        if (prependField(itemNode, SourceField.name, inner)) {\n            maybeAddBlankLine(text);\n        }\n        text.addAll(inner);\n    }\n\n    default String flattenToString(JsonNode node) {\n        return flattenToString(node, \"\\n\");\n    }\n\n    default String flattenToString(JsonNode node, String join) {\n        List<String> text = new ArrayList<>();\n        appendToText(text, node, null);\n        return String.join(join, text);\n    }\n\n    /**\n     * Maybe add a blank line to the list containing parsed text.\n     * Imperfect, but only add a blank line if the previous line is\n     * not already blank.\n     *\n     * @param text Text to analyze and maybe add a blank line to\n     */\n    default void maybeAddBlankLine(List<String> text) {\n        if (!text.isEmpty() && !text.get(text.size() - 1).isBlank()) {\n            text.add(\"\");\n        }\n    }\n\n    /**\n     * Returns a string which contains the backticks required to create an\n     * admonition around the given {@code content}. Internal / recursive parse.\n     */\n    default String nestedEmbed(List<String> content) {\n        int embedDepth = content.stream()\n                .map(String::trim)\n                .filter(s -> s.matches(\"`+\"))\n                .map(String::length)\n                .max(Integer::compare).orElse(2);\n        char[] ticks = new char[embedDepth + 1];\n        Arrays.fill(ticks, '`');\n        return new String(ticks);\n    }\n\n    /** Internal / recursive parse */\n    default boolean prependField(JsonNode entry, JsonNodeReader field, List<String> inner) {\n        String n = field.replaceTextFrom(entry, this);\n        return prependField(n, inner);\n    }\n\n    default boolean prependField(String name, List<String> inner) {\n        if (isPresent(name)) {\n            name = replaceText(name.trim());\n            if (inner.isEmpty()) {\n                inner.add(name);\n            } else if (inner.get(0).startsWith(\"|\") || inner.get(0).startsWith(\">\")) {\n                // we have a table or a blockquote\n                name = \"**\" + name + \"** \";\n                inner.add(0, \"\");\n                inner.add(0, name);\n                return true;\n            } else {\n                name = name.replace(\":\", \"\");\n                name = \"**\" + name + \".** \";\n                inner.set(0, name + inner.get(0));\n                return true;\n            }\n        }\n        return false;\n    }\n\n    /** Internal / recursive parse */\n    default void prependText(String prefix, List<String> inner) {\n        if (inner.isEmpty()) {\n            inner.add(prefix);\n        } else if (inner.get(0).isEmpty() && inner.size() > 1) {\n            // leading blank line\n            inner.set(1, prependText(prefix, inner.get(1)));\n        } else {\n            // update first line\n            inner.set(0, prependText(prefix, inner.get(0)));\n        }\n    }\n\n    /** Internal / recursive parse */\n    default String prependText(String prefix, String text) {\n        return text.startsWith(prefix) ? text : prefix + text;\n    }\n\n    /** Internal / recursive parse */\n    default void prependTextMakeListItem(List<String> text, JsonNode e, String prepend, String continuation) {\n        List<String> inner = new ArrayList<>();\n        appendToText(inner, e, null);\n        if (inner.size() > 0) {\n            prependText(\"- \" + prepend, inner);\n            inner.forEach(i -> text.add(i.startsWith(\"-\") || i.isBlank() ? i : continuation + i));\n        }\n    }\n\n    /** Internal / recursive parse */\n    default List<String> removePreamble(List<String> content) {\n        if (content == null || content.isEmpty()) {\n            return List.of();\n        }\n        boolean hasYaml = content.get(0).equals(\"---\");\n        int endYaml = -1;\n        int start = -1;\n        for (int i = 0; i < content.size(); i++) {\n            String line = content.get(i);\n            if (line.equals(\"---\") && hasYaml && i > 0 && endYaml < 0) {\n                endYaml = i;\n            } else if (line.startsWith(\"%%--\")) {\n                start = i;\n                break;\n            }\n        }\n        if (start < 0 && endYaml > 0) {\n            start = endYaml; // if no other marker present, lop off the yaml header\n        }\n        if (start >= 0) {\n            // remove until start\n            content.subList(0, start + 1).clear();\n        }\n        return content;\n    }\n\n    /**\n     * Return the rendered contents of the specified resource\n     *\n     * @param resource QuteBase containing required template resource data\n     * @param admonition Type of embedded/encapsulating admonition\n     */\n    default String renderEmbeddedTemplate(QuteBase resource, String admonition) {\n        List<String> inner = new ArrayList<>();\n        renderEmbeddedTemplate(inner, resource, admonition, List.of());\n        return String.join(\"\\n\", inner);\n    }\n\n    /**\n     * Embed rendered contents of the specified resource\n     *\n     * @param text List of text content should be added to\n     * @param resource QuteBase containing required template resource data\n     * @param admonition Type of embedded/encapsulating admonition\n     * @param prepend Text to prepend at beginning of admonition (e.g. title)\n     */\n    default void renderEmbeddedTemplate(List<String> text, QuteBase resource, String admonition, List<String> prepend) {\n        boolean pushed = parseState().push(resource.sources());\n        try {\n            String rendered = tui().renderEmbedded(resource);\n            List<String> inner = new ArrayList<>(prepend);\n            inner.addAll(removePreamble(new ArrayList<>(List.of(rendered.split(\"\\n\")))));\n\n            maybeAddBlankLine(text);\n            if (admonition != null) {\n                wrapAdmonition(inner, \"embed-\" + admonition);\n            } else {\n                balanceBackticks(inner);\n            }\n            text.addAll(inner);\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    /**\n     * Return the rendered contents of an (always) inline template.\n     *\n     * @param resource QuteUtil containing required template resource data\n     * @param admonition Type of inline admonition\n     */\n    default String renderInlineTemplate(QuteUtil resource, String admonition) {\n        List<String> inner = new ArrayList<>();\n        renderInlineTemplate(inner, resource, admonition);\n        return String.join(\"\\n\", inner);\n    }\n\n    /**\n     * Add rendered contents of an (always) inline template\n     * to collected text\n     *\n     * @param text List of text content should be added to\n     * @param resource QuteUtil containing required template resource data\n     * @param admonition Type of inline admonition\n     */\n    default void renderInlineTemplate(List<String> text, QuteUtil resource, String admonition) {\n        String rendered = tui().renderEmbedded(resource);\n        List<String> inner = removePreamble(new ArrayList<>(List.of(rendered.split(\"\\n\"))));\n\n        maybeAddBlankLine(text);\n        if (admonition != null) {\n            wrapAdmonition(inner, \"inline-\" + admonition);\n        } else {\n            balanceBackticks(inner);\n        }\n        text.addAll(inner);\n    }\n\n    /** Wrap {@code inner} in an admonition with the name {@code admonition}. */\n    default void wrapAdmonition(List<String> inner, String admonition) {\n        if (admonition == null || admonition.isEmpty() || inner == null || inner.isEmpty()) {\n            return;\n        }\n        String backticks = nestedEmbed(inner);\n        inner.add(0, backticks + \"ad-\" + admonition);\n        inner.add(inner.size(), backticks);\n    }\n\n    /**\n     * Add backticks to the outermost admonition in {@code inner} so that it\n     * correctly wraps the rest of the text. Has no effect if the first non-empty\n     * line of {@code inner} is not an admonition.\n     */\n    private void balanceBackticks(List<String> inner) {\n        int[] indices = outerAdmonitionIndices(inner);\n        if (indices == null) {\n            // There was no outer admonition, so do nothing.\n            return;\n        }\n        int adEndIdx = indices[1];\n        int adStartIdx = indices[0];\n        String firstLine = inner.get(adStartIdx);\n        // Must be done in this order so the indices don't change\n        inner.remove(adEndIdx);\n        inner.remove(adStartIdx);\n        String backticks = nestedEmbed(inner);\n        inner.add(adStartIdx, backticks + firstLine.replaceFirst(\"^`+\", \"\"));\n        inner.add(adEndIdx, backticks);\n    }\n\n    /**\n     * Return the indices of the start and end of the admonition which wraps {@code inner}, or null if there is no\n     * admonition.\n     */\n    default int[] outerAdmonitionIndices(List<String> inner) {\n        int[] presentIndices = IntStream.range(0, inner.size())\n                .filter(idx -> isPresent(inner.get(idx)))\n                .toArray();\n        if (presentIndices.length < 2) {\n            // We need at least two non-empty lines to have one each for the opening and closing\n            // admonition lines.\n            return null;\n        }\n        int firstLineIdx = presentIndices[0];\n\n        String firstLine = inner.get(firstLineIdx);\n        if (!firstLine.matches(\"```+ad[\\\\s\\\\S]+\")) {\n            return null;\n        }\n        int lastLineIdx = presentIndices[presentIndices.length - 1];\n        String lastLine = inner.get(lastLineIdx);\n        if (!lastLine.matches(\"```+\")) {\n            // we expect the last non-empty line to contain the closing set of backticks\n            tui().debugf(\"Expected line %d to close backticks but was instead '%s'\", lastLineIdx, lastLine);\n            return null;\n        }\n        return new int[] { firstLineIdx, lastLineIdx };\n    }\n\n    String replaceText(String s);\n\n    default String replaceText(JsonNode input) {\n        if (input == null) {\n            return null;\n        }\n        if (input.isObject() || input.isArray()) {\n            throw new IllegalArgumentException(\"Can only replace text for textual nodes: \" + input);\n        }\n        return replaceText(input.asText());\n    }\n\n    default String slugify(String s) {\n        return Tui.slugify(s);\n    }\n\n    default ArrayNode ensureArray(JsonNode source) {\n        if (source == null || source.isNull()) {\n            return Tui.MAPPER.createArrayNode();\n        }\n        if (source.isArray()) {\n            return (ArrayNode) source;\n        }\n        return Tui.MAPPER.createArrayNode().add(source);\n    }\n\n    default ObjectNode ensureObjectNode(JsonNode source) {\n        if (source == null || source.isNull()) {\n            return Tui.MAPPER.createObjectNode();\n        }\n        if (source.isArray()) {\n            throw new IllegalArgumentException(\"Can not make an ObjectNode from an ArrayNode\");\n        }\n        return (ObjectNode) source;\n    }\n\n    default Stream<JsonNode> streamOf(JsonNode source) {\n        if (source == null || source.isNull()) {\n            return Stream.of();\n        }\n        if (source.isObject()) {\n            return Stream.of(source);\n        }\n        return StreamSupport.stream(iterableElements(source).spliterator(), false);\n    }\n\n    default Stream<String> streamOfFieldNames(JsonNode source) {\n        if (source == null) {\n            return Stream.of();\n        }\n        return StreamSupport.stream(iterableFieldNames(source).spliterator(), false);\n    }\n\n    default Stream<Entry<String, JsonNode>> streamProps(JsonNode source) {\n        return streamPropsExcluding(source, (JsonNodeReader[]) null);\n    }\n\n    default Stream<Entry<String, JsonNode>> streamPropsExcluding(JsonNode source, JsonNodeReader... excludingKeys) {\n        if (source == null || !source.isObject()) {\n            return Stream.of();\n        }\n        if (excludingKeys == null || excludingKeys.length == 0) {\n            return source.properties().stream();\n        }\n        return source.properties().stream()\n                .filter(e -> Arrays.stream(excludingKeys).noneMatch(s -> e.getKey().equalsIgnoreCase(s.name())));\n    }\n\n    /** {@link #createLink(String, Path, String, String)} with an empty title */\n    default String createLink(String displayText, Path target, String anchor) {\n        return createLink(displayText, target, anchor, null);\n    }\n\n    /**\n     * Return a string with a markdown formatted link from the given components.\n     *\n     * @param displayText The display text to use for the link\n     * @param target The target path that the link should point to\n     * @param anchor An anchor to add to the link\n     * @param title A title to use for the link\n     */\n    default String createLink(String displayText, Path target, String anchor, String title) {\n        if (target == null) {\n            throw new IllegalArgumentException(\"Can't create link with null path\");\n        }\n        title = title != null ? \"\\\"%s\\\"\".formatted(title) : null;\n        String targetPath = join(\"#\", target.endsWith(\".md\") ? target : (target + \".md\"), toAnchorTag(anchor));\n        return \"[%s](%s)\".formatted(displayText, join(\" \", targetPath, title))\n                // Get rid of Windows-style path separators - they break links in Obsidian\n                .replace('\\\\', '/');\n    }\n\n    default List<String> toListOfStrings(JsonNode source) {\n        if (source == null) {\n            return List.of();\n        } else if (source.isTextual()) {\n            return List.of(source.asText());\n        }\n        List<String> list = tui().readJsonValue(source, Tui.LIST_STRING);\n        return list == null ? List.of() : list;\n    }\n\n    enum SourceField implements JsonNodeReader {\n        abbreviation,\n        _class_(\"class\"),\n        entry,\n        entries,\n        id,\n        items,\n        _meta,\n        isReprinted,\n        name,\n        note,\n        page,\n        reprintedAs,\n        source,\n        tag,\n        type,\n        uid;\n\n        final String nodeName;\n\n        SourceField() {\n            this.nodeName = this.name();\n        }\n\n        SourceField(String nodeName) {\n            this.nodeName = nodeName;\n        }\n\n        public String nodeName() {\n            return nodeName;\n        }\n\n        @Override\n        public String getTextOrDefault(JsonNode x, String value) {\n            String text = JsonNodeReader.super.getTextOrDefault(x, value);\n            return this == name\n                    ? text.replace(\"\\u00A0\", \"\").trim()\n                    : text;\n        }\n\n        @Override\n        public String getTextOrEmpty(JsonNode x) {\n            String text = JsonNodeReader.super.getTextOrEmpty(x);\n            return this == name\n                    ? text.replace(\"\\u00A0\", \"\").trim()\n                    : text;\n        }\n\n        @Override\n        public String getTextOrNull(JsonNode x) {\n            String text = JsonNodeReader.super.getTextOrNull(x);\n            return this == name && text != null\n                    ? text.replace(\"\\u00A0\", \"\").trim()\n                    : text;\n        }\n\n        @Override\n        public String replaceTextFrom(JsonNode node, JsonTextConverter<?> replacer) {\n            String text = JsonNodeReader.super.replaceTextFrom(node, replacer);\n            return this == name && text != null\n                    ? text.replace(\"\\u00A0\", \"\").trim()\n                    : text;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/MarkdownConverter.java",
    "content": "package dev.ebullient.convert.tools;\n\nimport java.util.List;\n\npublic interface MarkdownConverter {\n\n    MarkdownConverter writeAll();\n\n    MarkdownConverter writeFiles(List<? extends IndexType> types);\n\n    MarkdownConverter writeFiles(IndexType type);\n\n    MarkdownConverter writeImages();\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/ParseState.java",
    "content": "package dev.ebullient.convert.tools;\n\nimport java.util.ArrayDeque;\nimport java.util.Deque;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.config.CompendiumConfig.DiceRoller;\nimport dev.ebullient.convert.config.TtrpgConfig;\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.qute.SourceAndPage;\nimport dev.ebullient.convert.tools.dnd5e.Tools5eIndexType;\nimport dev.ebullient.convert.tools.pf2e.Pf2eIndexType;\n\npublic class ParseState {\n\n    enum ParseStateField implements JsonNodeReader {\n        page,\n        source\n    }\n\n    public static class ParseStateInfo {\n        boolean inFootnotes;\n        boolean inHtmlTable;\n        boolean inMarkdownTable;\n        boolean inList;\n        boolean inTrait;\n        int inFeatureType;\n        final String listIndent;\n        final String src;\n        final int page;\n\n        private ParseStateInfo() {\n            this(null, 0, \"\");\n        }\n\n        private ParseStateInfo(String src) {\n            this(src, 0, \"\");\n        }\n\n        private ParseStateInfo(String src, int page) {\n            this(src, page, \"\");\n        }\n\n        private ParseStateInfo(String src, int page, String listIndent) {\n            this.src = src;\n            this.listIndent = listIndent;\n            this.page = page;\n        }\n\n        private ParseStateInfo setInList(boolean inList) {\n            this.inList = inList;\n            return this;\n        }\n\n        private ParseStateInfo setInFootnotes(boolean inFootnotes) {\n            this.inFootnotes = inFootnotes;\n            return this;\n        }\n\n        private ParseStateInfo setInHtmlTable(boolean inTable) {\n            this.inHtmlTable = inTable;\n            return this;\n        }\n\n        private ParseStateInfo setInMarkdownTable(boolean inTable) {\n            this.inMarkdownTable = inTable;\n            return this;\n        }\n\n        private ParseStateInfo setInFeatureType(int inFeatureType) {\n            this.inFeatureType = inFeatureType;\n            return this;\n        }\n\n        private ParseStateInfo setInTrait(boolean inTrait) {\n            this.inTrait = inTrait;\n            return this;\n        }\n\n        private ParseStateInfo setTheRest(ParseStateInfo prev) {\n            this.setInFootnotes(prev.inFootnotes)\n                    .setInHtmlTable(prev.inHtmlTable)\n                    .setInMarkdownTable(prev.inMarkdownTable)\n                    .setInList(prev.inList)\n                    .setInFeatureType(prev.inFeatureType)\n                    .setInTrait(prev.inTrait);\n            return this;\n        }\n\n        private static ParseStateInfo srcAndPage(ParseStateInfo prev, String src, int page) {\n            if (prev == null) {\n                return new ParseState.ParseStateInfo(src, page);\n            }\n            return new ParseState.ParseStateInfo(\n                    src == null ? prev.src : src,\n                    page,\n                    prev.listIndent)\n                    .setTheRest(prev);\n        }\n\n        private static ParseStateInfo changePage(ParseStateInfo prev, int page) {\n            if (prev == null) {\n                Tui.instance().errorf(\"Change Page called without someone setting the source first? %s\");\n                return null;\n            }\n            return new ParseStateInfo(prev.src, page, prev.listIndent)\n                    .setTheRest(prev);\n        }\n\n        private static ParseStateInfo inFootnotes(ParseStateInfo prev, boolean inFootnotes) {\n            if (prev == null) {\n                return new ParseStateInfo().setInFootnotes(inFootnotes);\n            }\n            return new ParseState.ParseStateInfo(prev.src, prev.page, prev.listIndent)\n                    .setTheRest(prev)\n                    .setInFootnotes(inFootnotes);\n        }\n\n        private static ParseStateInfo inHtmlTable(ParseStateInfo prev, boolean inHtmlTable) {\n            if (prev == null) {\n                return new ParseStateInfo().setInHtmlTable(inHtmlTable);\n\n            }\n            return new ParseState.ParseStateInfo(prev.src, prev.page, prev.listIndent)\n                    .setTheRest(prev)\n                    .setInHtmlTable(inHtmlTable);\n        }\n\n        private static ParseStateInfo inMarkdownTable(ParseStateInfo prev, boolean inMarkdownTable) {\n            if (prev == null) {\n                return new ParseStateInfo().setInMarkdownTable(inMarkdownTable);\n\n            }\n            return new ParseState.ParseStateInfo(prev.src, prev.page, prev.listIndent)\n                    .setTheRest(prev)\n                    .setInMarkdownTable(inMarkdownTable);\n        }\n\n        private static ParseStateInfo indentList(ParseStateInfo prev) {\n            if (prev == null) {\n                return new ParseStateInfo().setInList(true);\n            }\n            return new ParseState.ParseStateInfo(prev.src, prev.page, prev.listIndent + \"    \")\n                    .setTheRest(prev)\n                    .setInList(true);\n        }\n\n        private static ParseStateInfo indentList(ParseStateInfo prev, String value) {\n            if (prev == null) {\n                return new ParseStateInfo().setInList(true);\n            }\n            return new ParseState.ParseStateInfo(prev.src, prev.page, value)\n                    .setTheRest(prev)\n                    .setInList(true);\n        }\n\n        private static ParseStateInfo pushFeatureType(ParseStateInfo prev) {\n            if (prev == null) {\n                return new ParseStateInfo().setInFeatureType(0);\n            }\n            return new ParseState.ParseStateInfo(prev.src, prev.page, prev.listIndent)\n                    .setTheRest(prev)\n                    .setInFeatureType(prev.inFeatureType + 1);\n        }\n\n        private static ParseStateInfo pushTrait(ParseStateInfo prev) {\n            if (prev == null) {\n                return new ParseStateInfo().setInTrait(true);\n            }\n            return new ParseState.ParseStateInfo(prev.src, prev.page, prev.listIndent)\n                    .setTheRest(prev)\n                    .setInTrait(true);\n        }\n    }\n\n    private final Deque<ParseState.ParseStateInfo> stack = new ArrayDeque<>();\n    private final Map<String, String> citations = new HashMap<>();\n\n    public boolean push(CompendiumSources sources, JsonNode rootNode) {\n        if (rootNode != null && (rootNode.has(\"page\") || rootNode.has(\"source\"))) {\n            return push(rootNode);\n        }\n        return push(sources);\n    }\n\n    public boolean push(CompendiumSources sources) {\n        if (sources == null) {\n            return false;\n        }\n        return push(sources.findNode());\n    }\n\n    public boolean push(JsonNode node) {\n        String src = ParseStateField.source.getTextOrNull(node);\n        int page = ParseStateField.page.intOrDefault(node, 0);\n        return push(src, page);\n    }\n\n    public boolean push(String src, int page) {\n        if (src == null && page == 0) {\n            return false;\n        }\n        ParseStateInfo current = stack.peek();\n        if (current == null) {\n            ParseStateInfo info = ParseStateInfo.srcAndPage(current, src, page);\n            stack.addFirst(info);\n            return true;\n        }\n        if (src != null && !src.equals(current.src)) {\n            ParseState.ParseStateInfo info = ParseStateInfo.srcAndPage(current, src, page);\n            stack.addFirst(info);\n            return true;\n        } else if (page != 0 && page != current.page) {\n            ParseStateInfo info = ParseStateInfo.changePage(current, page);\n            stack.addFirst(info);\n            return true;\n        } else\n            return false;\n    }\n\n    public boolean pushFootnotes(boolean inFootnotes) {\n        stack.addFirst(ParseStateInfo.inFootnotes(stack.peek(), inFootnotes));\n        return true;\n    }\n\n    public boolean pushHtmlTable(boolean inTable) {\n        stack.addFirst(ParseStateInfo.inHtmlTable(stack.peek(), inTable));\n        return true;\n    }\n\n    public boolean pushMarkdownTable(boolean inTable) {\n        ParseStateInfo current = stack.peek();\n        if (current != null && current.inMarkdownTable == inTable) {\n            return false;\n        }\n        stack.addFirst(ParseStateInfo.inMarkdownTable(stack.peek(), inTable));\n        return true;\n    }\n\n    public boolean indentList() {\n        stack.addFirst(ParseStateInfo.indentList(stack.peek()));\n        return true;\n    }\n\n    public boolean indentList(String value) {\n        stack.addFirst(ParseStateInfo.indentList(stack.peek(), value));\n        return true;\n    }\n\n    public boolean pushFeatureType() {\n        stack.addFirst(ParseStateInfo.pushFeatureType(stack.peek()));\n        return true;\n    }\n\n    public boolean pushTrait() {\n        stack.addFirst(ParseStateInfo.pushTrait(stack.peek()));\n        return true;\n    }\n\n    public void pop(boolean pushed) {\n        if (pushed) {\n            String source = sourcePageString();\n            ParseStateInfo removed = stack.removeFirst();\n            if (stack.isEmpty() && !citations.isEmpty()) {\n                Tui.instance().errorf(\"%s left unreferenced citations behind\", source.isEmpty() ? removed : source);\n                citations.clear();\n            }\n        }\n    }\n\n    public String getListIndent() {\n        ParseState.ParseStateInfo current = stack.peek();\n        return current == null ? \"\" : current.listIndent;\n    }\n\n    public boolean inFootnotes() {\n        ParseState.ParseStateInfo current = stack.peek();\n        return current != null && current.inFootnotes;\n    }\n\n    public boolean inList() {\n        ParseState.ParseStateInfo current = stack.peek();\n        return current != null && current.inList;\n    }\n\n    public boolean inHtmlTable() {\n        ParseState.ParseStateInfo current = stack.peek();\n        return current != null && current.inHtmlTable;\n    }\n\n    public boolean inMarkdownTable() {\n        ParseState.ParseStateInfo current = stack.peek();\n        return current != null && current.inMarkdownTable;\n    }\n\n    public boolean inTable() {\n        ParseState.ParseStateInfo current = stack.peek();\n        return current != null && (current.inHtmlTable || current.inMarkdownTable);\n    }\n\n    public boolean inTrait() {\n        ParseState.ParseStateInfo current = stack.peek();\n        return current != null && current.inTrait;\n    }\n\n    public int featureTypeDepth() {\n        ParseState.ParseStateInfo current = stack.peek();\n        return current == null ? 0 : current.inFeatureType;\n    }\n\n    public String sourcePageString() {\n        ParseState.ParseStateInfo current = stack.peek();\n        if (current == null || current.page == 0) {\n            return \"\";\n        }\n        return String.format(\"%s p. %s\",\n                current.src, current.page);\n    }\n\n    public String sourcePageString(String formatString) {\n        ParseState.ParseStateInfo current = stack.peek();\n        if (current == null || current.page == 0) {\n            return \"\";\n        }\n        return String.format(formatString, current.src, current.page);\n    }\n\n    public String longSourcePageString(String formatString) {\n        ParseState.ParseStateInfo current = stack.peek();\n        if (current == null || current.page == 0) {\n            return \"\";\n        }\n        return String.format(formatString,\n                TtrpgConfig.sourceToLongName(current.src), current.page);\n    }\n\n    public String getSource() {\n        ParseState.ParseStateInfo current = stack.peek();\n        return current == null ? null : current.src;\n    }\n\n    public String getSource(Tools5eIndexType type) {\n        ParseState.ParseStateInfo current = stack.peek();\n        if (current == null || current.src == null) {\n            return type == null ? null : type.defaultSourceString();\n        }\n        return current.src;\n    }\n\n    public String getSource(Pf2eIndexType type) {\n        ParseState.ParseStateInfo current = stack.peek();\n        if (current == null || current.src == null) {\n            return type == null ? null : type.defaultSourceString();\n        }\n        return current.src;\n    }\n\n    public String getPage() {\n        ParseState.ParseStateInfo current = stack.peek();\n        return current == null ? null : current.page + \"\";\n    }\n\n    public SourceAndPage toSourceAndPage() {\n        return new SourceAndPage(getSource(), getPage());\n    }\n\n    public void addCitation(String key, String citationText) {\n        String old = citations.put(key, citationText);\n        if (old != null && !old.equals(citationText)) {\n            Tui.instance().errorf(\"Duplicate citation text for %s:\\nOLD:\\n%s\\nNEW:\\n%s\", key, old, citationText);\n        }\n    }\n\n    public void popCitations(List<String> footerEntries) {\n        citations.forEach((k, v) -> {\n            if (v.startsWith(\"|\")) { // we have a table, assume noted thing is in the footnote\n                footerEntries.add(v);\n                return;\n            }\n            footerEntries.add(String.format(\"[%s]: %s\",\n                    k, v));\n        });\n        citations.clear();\n    }\n\n    public DiceFormulaState diceFormulaState() {\n        return new DiceFormulaState(this);\n    }\n\n    public static class DiceFormulaState {\n        public final DiceRoller roller;\n        public final boolean suppressInYaml;\n\n        public DiceFormulaState(ParseState parseState) {\n            this.roller = TtrpgConfig.getConfig().useDiceRoller();\n            this.suppressInYaml = parseState.inTrait() && roller.useFantasyStatblocks();\n        }\n\n        /**\n         * We can't use dice roller fomulas if the roller is disabled, or if we're\n         * in a YAML trait block.\n         */\n        public boolean noRoller() {\n            return !roller.enabled() || suppressInYaml;\n        }\n\n        /** In YAML blocks (traits), we avoid all formatting in dice formulas */\n        public boolean plainText() {\n            return suppressInYaml;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/Tags.java",
    "content": "package dev.ebullient.convert.tools;\n\nimport static dev.ebullient.convert.StringUtil.join;\n\nimport java.util.List;\nimport java.util.Set;\nimport java.util.TreeSet;\n\nimport dev.ebullient.convert.config.CompendiumConfig;\nimport dev.ebullient.convert.config.TtrpgConfig;\n\npublic class Tags {\n    private final Set<String> tags = new TreeSet<>();\n    private final CompendiumConfig config;\n\n    public Tags() {\n        this(null);\n    }\n\n    public Tags(CompendiumSources sources) {\n        this.config = TtrpgConfig.getConfig();\n        addSourceTags(sources);\n    }\n\n    public void addSourceTags(CompendiumSources sources) {\n        if (sources != null) {\n            String sourceTag = config.tagOfRaw(sources.primarySourceTag());\n            tags.add(sourceTag);\n        }\n    }\n\n    /** Prepend configured prefix and slugify parts */\n    public void addRaw(String... rawValues) {\n        String rawTag = config.tagOfRaw(join(\"/\", List.of(rawValues)));\n        tags.add(rawTag);\n    }\n\n    /** Prepend configured prefix and slugify parts */\n    public void add(String... segments) {\n        String tag = config.tagOf(segments);\n        tags.add(tag);\n    }\n\n    public Set<String> build() {\n        return tags;\n    }\n\n    @Override\n    public String toString() {\n        return \"Tags [tags=\" + tags + \"]\";\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/ToolsIndex.java",
    "content": "package dev.ebullient.convert.tools;\n\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.util.Objects;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.config.CompendiumConfig;\nimport dev.ebullient.convert.config.Datasource;\nimport dev.ebullient.convert.config.TtrpgConfig;\nimport dev.ebullient.convert.io.MarkdownWriter;\nimport dev.ebullient.convert.tools.dnd5e.Tools5eIndex;\nimport dev.ebullient.convert.tools.pf2e.Pf2eIndex;\n\npublic interface ToolsIndex {\n    // Special one-offs for accounting/tracking\n    enum TtrpgValue implements JsonNodeReader {\n        indexBaseItem,\n        indexFluffKey,\n        indexInputType,\n        indexKey,\n        indexParentKey,\n        indexVersionKeys,\n        isHomebrew,\n        homebrewSource,\n        homebrewBaseSource,\n    }\n\n    static ToolsIndex createIndex() {\n        CompendiumConfig config = TtrpgConfig.getConfig();\n        return createIndex(config.datasource(), config);\n    }\n\n    static ToolsIndex createIndex(Datasource game, CompendiumConfig config) {\n        if (Objects.requireNonNull(game) == Datasource.toolsPf2e) {\n            return new Pf2eIndex(config);\n        }\n        return new Tools5eIndex(config);\n    }\n\n    CompendiumConfig cfg();\n\n    default String rulesVaultRoot() {\n        return cfg().rulesVaultRoot();\n    }\n\n    default String compendiumVaultRoot() {\n        return cfg().compendiumVaultRoot();\n    }\n\n    default Path rulesFilePath() {\n        return cfg().rulesFilePath();\n    }\n\n    default Path compendiumFilePath() {\n        return cfg().compendiumFilePath();\n    }\n\n    default boolean resolveSources(Path toolsPath) {\n        // Check for a 'data' subdirectory\n        Path data = toolsPath.resolve(\"data\");\n        if (data.toFile().isDirectory()) {\n            toolsPath = data;\n        }\n\n        TtrpgConfig.setToolsPath(toolsPath);\n        var allOk = true;\n        for (String adventure : cfg().resolveAdventures()) {\n            allOk &= cfg().readSource(toolsPath.resolve(adventure), TtrpgConfig.getFixes(adventure), this::importTree);\n        }\n        for (String book : cfg().resolveBooks()) {\n            allOk &= cfg().readSource(toolsPath.resolve(book), TtrpgConfig.getFixes(book), this::importTree);\n        }\n        // Include additional standalone files from config (relative to current directory)\n        for (String brew : cfg().resolveHomebrew()) {\n            allOk &= cfg().readSource(Path.of(brew), TtrpgConfig.getFixes(brew), this::importTree);\n        }\n        return allOk;\n    }\n\n    void prepare();\n\n    boolean notPrepared();\n\n    ToolsIndex importTree(String filename, JsonNode node);\n\n    MarkdownConverter markdownConverter(MarkdownWriter writer);\n\n    void writeFullIndex(Path resolve) throws IOException;\n\n    void writeFilteredIndex(Path resolve) throws IOException;\n\n    JsonNode getBook(String b);\n\n    JsonNode getAdventure(String a);\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/BackgroundTraits2Note.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.qute.QuteNote;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteNote;\n\npublic class BackgroundTraits2Note extends Json2QuteCommon {\n\n    final String targetDir = linkifier().getRelativePath(Tools5eIndexType.table);\n\n    public BackgroundTraits2Note(Tools5eIndex index) {\n        super(index, Tools5eIndexType.syntheticGroup, null);\n    }\n\n    public List<QuteNote> buildNotes() {\n        List<QuteNote> notes = new ArrayList<>();\n\n        addIfPresent(notes, Json2QuteBackground.traits, \"Personality Traits\");\n        addIdealsIfPresent(notes);\n        addIfPresent(notes, Json2QuteBackground.bonds, \"Bonds\");\n        addIfPresent(notes, Json2QuteBackground.flaws, \"Flaws\");\n\n        return notes;\n    }\n\n    private void addIfPresent(List<QuteNote> notes, Set<String> table, String title) {\n        if (table.isEmpty()) {\n            return;\n        }\n        boolean pushed = parseState().push(getSources(), rootNode);\n        try {\n            List<String> rows = table.stream()\n                    .filter(x -> x.startsWith(\"|\") && !x.contains(\"---\"))\n                    .map(x -> x.replaceAll(\"^\\\\|\\\\s*[\\\\d-]+\\\\s*\", \"\"))\n                    .collect(Collectors.toList());\n\n            String slug = slugify(title);\n            String blockid = \"^\" + slug;\n            List<String> text = new ArrayList<>();\n            text.add(String.format(\"`dice: [](%s.md#%s)`\", slug, blockid));\n            text.add(\"\");\n            text.addAll(listToTable(title, rows));\n\n            notes.add(new Tools5eQuteNote(title, null, text, new Tags())\n                    .withTargetPath(targetDir));\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    private void addIdealsIfPresent(List<QuteNote> notes) {\n        if (Json2QuteBackground.ideals.isEmpty()) {\n            return;\n        }\n        boolean pushed = parseState().push(getSources(), rootNode);\n        try {\n            List<String> ideals = Json2QuteBackground.ideals.stream()\n                    .map(x -> x.replace(\"**\", \"\"))\n                    .map(x -> x.replaceAll(\"^\\\\|\\\\s*\\\\d+\\\\s*\", \"\"))\n                    .toList();\n\n            List<String> good = ideals.stream().filter(x -> x.contains(\"(Good)\"))\n                    .collect(Collectors.toList());\n            List<String> evil = ideals.stream().filter(x -> x.contains(\"(Evil)\"))\n                    .collect(Collectors.toList());\n            List<String> lawful = ideals.stream().filter(x -> x.contains(\"(Lawful)\"))\n                    .collect(Collectors.toList());\n            List<String> chaotic = ideals.stream().filter(x -> x.contains(\"(Chaotic)\"))\n                    .collect(Collectors.toList());\n            List<String> neutral = ideals.stream().filter(x -> x.contains(\"(Neutral)\"))\n                    .collect(Collectors.toList());\n            List<String> any = ideals.stream().filter(x -> x.contains(\"(Any)\"))\n                    .collect(Collectors.toList());\n\n            List<String> text = new ArrayList<>();\n\n            text.add(\"| All Ideals |\");\n            text.add(\"|------------|\");\n            text.add(\"| `dice: [](ideals.md#^good-ideals)` \" + \" |\");\n            text.add(\"| `dice: [](ideals.md#^evil-ideals)` \" + \" |\");\n            text.add(\"| `dice: [](ideals.md#^lawful-ideals)` \" + \" |\");\n            text.add(\"| `dice: [](ideals.md#^chaotic-ideals)` \" + \" |\");\n            text.add(\"| `dice: [](ideals.md#^neutral-ideals)` \" + \" |\");\n            text.add(\"| `dice: [](ideals.md#^universal-ideals-any)` \" + \" |\");\n            text.add(\"^all-ideals\");\n            text.add(\"\");\n            text.add(\"| Chaotic Good Ideals |\");\n            text.add(\"|------------|\");\n            text.add(\"| `dice: [](ideals.md#^good-ideals)` \" + \" |\");\n            text.add(\"| `dice: [](ideals.md#^chaotic-ideals)` \" + \" |\");\n            text.add(\"| `dice: [](ideals.md#^universal-ideals-any)` \" + \" |\");\n            text.add(\"^cg-ideals\");\n            text.add(\"\");\n            text.add(\"| Chaotic Evil Ideals |\");\n            text.add(\"|------------|\");\n            text.add(\"| `dice: [](ideals.md#^evil-ideals)` \" + \" |\");\n            text.add(\"| `dice: [](ideals.md#^chaotic-ideals)` \" + \" |\");\n            text.add(\"| `dice: [](ideals.md#^universal-ideals-any)` \" + \" |\");\n            text.add(\"^ce-ideals\");\n            text.add(\"\");\n            text.add(\"| Chaotic Neutral Ideals |\");\n            text.add(\"|------------|\");\n            text.add(\"| `dice: [](ideals.md#^neutral-ideals)` \" + \" |\");\n            text.add(\"| `dice: [](ideals.md#^chaotic-ideals)` \" + \" |\");\n            text.add(\"| `dice: [](ideals.md#^universal-ideals-any)` \" + \" |\");\n            text.add(\"^cn-ideals\");\n            text.add(\"\");\n            text.add(\"| Lawful Good Ideals |\");\n            text.add(\"|------------|\");\n            text.add(\"| `dice: [](ideals.md#^good-ideals)` \" + \" |\");\n            text.add(\"| `dice: [](ideals.md#^lawful-ideals)` \" + \" |\");\n            text.add(\"| `dice: [](ideals.md#^universal-ideals-any)` \" + \" |\");\n            text.add(\"^lg-ideals\");\n            text.add(\"\");\n            text.add(\"| Lawful Evil Ideals |\");\n            text.add(\"|------------|\");\n            text.add(\"| `dice: [](ideals.md#^evil-ideals)` \" + \" |\");\n            text.add(\"| `dice: [](ideals.md#^lawful-ideals)` \" + \" |\");\n            text.add(\"| `dice: [](ideals.md#^universal-ideals-any)` \" + \" |\");\n            text.add(\"^le-ideals\");\n            text.add(\"\");\n            text.add(\"| Lawful Neutral Ideals |\");\n            text.add(\"|------------|\");\n            text.add(\"| `dice: [](ideals.md#^neutral-ideals)` \" + \" |\");\n            text.add(\"| `dice: [](ideals.md#^lawful-ideals)` \" + \" |\");\n            text.add(\"| `dice: [](ideals.md#^universal-ideals-any)` \" + \" |\");\n            text.add(\"^ln-ideals\");\n            text.add(\"\");\n            text.add(\"| Neutral Ideals |\");\n            text.add(\"|------------|\");\n            text.add(\"| `dice: [](ideals.md#^neutral-ideals)` \" + \" |\");\n            text.add(\"| `dice: [](ideals.md#^universal-ideals-any)` \" + \" |\");\n            text.add(\"^n-ideals\");\n            text.add(\"\");\n            text.add(\"| Neutral Good Ideals |\");\n            text.add(\"|------------|\");\n            text.add(\"| `dice: [](ideals.md#^neutral-ideals)` \" + \" |\");\n            text.add(\"| `dice: [](ideals.md#^good-ideals)` \" + \" |\");\n            text.add(\"| `dice: [](ideals.md#^universal-ideals-any)` \" + \" |\");\n            text.add(\"^ng-ideals\");\n            text.add(\"\");\n            text.add(\"| Neutral Evil Ideals |\");\n            text.add(\"|------------|\");\n            text.add(\"| `dice: [](ideals.md#^neutral-ideals)` \" + \" |\");\n            text.add(\"| `dice: [](ideals.md#^evil-ideals)` \" + \" |\");\n            text.add(\"| `dice: [](ideals.md#^universal-ideals-any)` \" + \" |\");\n            text.add(\"^ne-ideals\");\n\n            maybeAddBlankLine(text);\n            text.addAll(tableSection(\"Good Ideals\", good));\n            maybeAddBlankLine(text);\n            text.addAll(tableSection(\"Evil Ideals\", evil));\n            maybeAddBlankLine(text);\n            text.addAll(tableSection(\"Lawful Ideals\", lawful));\n            maybeAddBlankLine(text);\n            text.addAll(tableSection(\"Chaotic Ideals\", chaotic));\n            maybeAddBlankLine(text);\n            text.addAll(tableSection(\"Neutral Ideals\", neutral));\n            maybeAddBlankLine(text);\n            text.addAll(tableSection(\"Universal Ideals (Any)\", any));\n\n            notes.add(new Tools5eQuteNote(\"Ideals\", null, text, new Tags())\n                    .withTargetPath(targetDir));\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    List<String> tableSection(String title, List<String> elements) {\n        List<String> section = listToTable(title, elements);\n        section.add(0, \"\");\n        section.add(0, \"## \" + title);\n        return section;\n    }\n\n    List<String> listToTable(String tableHeading, List<String> elements) {\n        String header = \"| \" + tableHeading + \" |\";\n        List<String> text = new ArrayList<>();\n        text.add(header);\n        text.add(header.replaceAll(\"[^|]\", \"-\"));\n        text.addAll(elements);\n        text.add(\"^\" + Tui.slugify(tableHeading));\n        return text;\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/HomebrewIndex.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static dev.ebullient.convert.StringUtil.isPresent;\n\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.Set;\nimport java.util.function.Consumer;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.config.CompendiumConfig;\nimport dev.ebullient.convert.config.TtrpgConfig;\nimport dev.ebullient.convert.io.Msg;\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.tools.JsonNodeReader;\nimport dev.ebullient.convert.tools.ToolsIndex.TtrpgValue;\nimport dev.ebullient.convert.tools.dnd5e.PsionicType.CustomPsionicType;\nimport dev.ebullient.convert.tools.dnd5e.SkillOrAbility.CustomSkillOrAbility;\nimport dev.ebullient.convert.tools.dnd5e.SpellSchool.CustomSpellSchool;\n\npublic class HomebrewIndex implements JsonSource {\n\n    private final Map<String, HomebrewMetaTypes> homebrewMetaTypes = new HashMap<>();\n    private final Tools5eIndex index;\n\n    HomebrewIndex(Tools5eIndex index) {\n        this.index = index;\n    }\n\n    public void importBrew(Consumer<HomebrewMetaTypes> processHomebrewTree) {\n        for (HomebrewMetaTypes homebrew : homebrewMetaTypes.values()) {\n            processHomebrewTree.accept(homebrew);\n            for (var featureType : homebrew.optionalFeatureTypes.keySet()) {\n                index.optFeatureIndex.addOptionalFeatureType(featureType, homebrew);\n            }\n        }\n    }\n\n    public Collection<HomebrewMetaTypes> getHomebrewMetaTypes(Tools5eSources sources) {\n        Map<String, HomebrewMetaTypes> metaTypes = new HashMap<>();\n        for (String src : sources.getSources()) {\n            HomebrewMetaTypes meta = homebrewMetaTypes.get(src);\n            if (meta != null) {\n                metaTypes.put(meta.primary, meta);\n            }\n        }\n        return metaTypes.values();\n    }\n\n    public HomebrewMetaTypes getHomebrewMetaTypes(String source) {\n        return homebrewMetaTypes.get(source);\n    }\n\n    public SkillOrAbility findHomebrewSkillOrAbility(String key, Tools5eSources sources) {\n        Collection<HomebrewMetaTypes> metaTypes = getHomebrewMetaTypes(sources);\n        for (HomebrewMetaTypes meta : metaTypes) {\n            SkillOrAbility skill = meta.getSkillType(key);\n            if (skill != null) {\n                return skill;\n            }\n        }\n        return null;\n    }\n\n    public SpellSchool findHomebrewSpellSchool(String code, Tools5eSources sources) {\n        Collection<HomebrewMetaTypes> metaTypes = getHomebrewMetaTypes(sources);\n        for (HomebrewMetaTypes meta : metaTypes) {\n            SpellSchool school = meta.getSpellSchool(code);\n            if (school != null) {\n                return school;\n            }\n        }\n        return SpellSchool.SchoolEnum.None;\n    }\n\n    public ItemType findHomebrewType(String abbreviation, Tools5eSources sources) {\n        Collection<HomebrewMetaTypes> metaTypes = getHomebrewMetaTypes(sources);\n        for (HomebrewMetaTypes meta : metaTypes) {\n            // key is lowercase abbreviation\n            JsonNode node = meta.getItemType(abbreviation);\n            if (node != null) {\n                return ItemType.fromNode(node);\n            }\n        }\n        return null;\n    }\n\n    public ItemMastery findHomebrewMastery(String name, Tools5eSources sources) {\n        Collection<HomebrewMetaTypes> metaTypes = getHomebrewMetaTypes(sources);\n        for (HomebrewMetaTypes meta : metaTypes) {\n            // key is lowercase name\n            JsonNode node = meta.getItemMastery(name);\n            if (node != null) {\n                return ItemMastery.fromNode(node);\n            }\n        }\n        return null;\n    }\n\n    public ItemProperty findHomebrewProperty(String code, Tools5eSources sources) {\n        Collection<HomebrewMetaTypes> metaTypes = getHomebrewMetaTypes(sources);\n        for (HomebrewMetaTypes meta : metaTypes) {\n            JsonNode node = meta.getItemProperty(code);\n            if (node != null) {\n                return ItemProperty.fromNode(node);\n            }\n        }\n        return null;\n    }\n\n    public void clear() {\n        homebrewMetaTypes.clear();\n    }\n\n    public boolean addHomebrewSourcesIfPresent(String filename, JsonNode brewNode) {\n        JsonNode meta = SourceField._meta.getFrom(brewNode);\n        JsonNode sources = HomebrewFields.sources.getFrom(meta);\n        if (sources == null || sources.size() == 0) {\n            return false;\n        }\n        Set<String> definedSources = new HashSet<>();\n\n        for (JsonNode s : iterableElements(sources)) {\n            String json = HomebrewFields.json.getTextOrNull(s);\n            if (json == null) {\n                tui().errorf(Msg.BREW, \"Source does not define json id: %s\", s);\n                continue;\n            }\n            String fullName = HomebrewFields.full.getTextOrEmpty(s);\n            String abbreviation = HomebrewFields.abbreviation.getTextOrEmpty(s);\n            if (fullName == null) {\n                tui().warnf(Msg.BREW, \"Homebrew source %s missing full name: %s\", json, fullName);\n            }\n            TtrpgConfig.addHomebrewSource(fullName, json, abbreviation); // define source\n            TtrpgConfig.includeAdditionalSource(json); // include source\n            definedSources.add(json);\n        }\n\n        HomebrewMetaTypes metaTypes = new HomebrewMetaTypes(definedSources,\n                filename, brewNode, Tools5eFields.edition.getTextOrDefault(meta, \"classic\"));\n\n        for (var src : definedSources) {\n            homebrewMetaTypes.compute(src, (k, v) -> {\n                if (v == null) {\n                    return metaTypes;\n                }\n                tui().errorf(Msg.BREW, \"Shared homebrew id %s: %s and %s; ignoring definition in %s\",\n                        src, v.filename, v.filename);\n                return v;\n            });\n        }\n\n        // --- From meta of homebrew ---\n\n        for (Entry<String, JsonNode> entry : HomebrewFields.optionalFeatureTypes.iterateFieldsFrom(meta)) {\n            metaTypes.setOptionalFeatureType(entry.getKey(), entry.getValue().asText());\n        }\n\n        // ignoring short names for spell schools and psionic types\n        for (Entry<String, JsonNode> entry : HomebrewFields.spellSchools.iterateFieldsFrom(meta)) {\n            metaTypes.setSpellSchool(entry.getKey(), entry.getValue());\n        }\n\n        for (Entry<String, JsonNode> entry : HomebrewFields.psionicTypes.iterateFieldsFrom(meta)) {\n            metaTypes.setPsionicType(entry.getKey(), entry.getValue());\n        }\n\n        Tools5eSources.addFonts(meta, HomebrewFields.fonts);\n\n        return true;\n    }\n\n    static class HomebrewMetaTypes {\n        final String primary;\n        final Set<String> sourceKeys;\n        final String filename;\n        final JsonNode homebrewNode;\n        final String edition;\n\n        // name, long name\n        final Map<String, String> optionalFeatureTypes = new HashMap<>();\n        final Map<String, PsionicType> psionicTypes = new HashMap<>();\n        final Map<String, SkillOrAbility> skillOrAbility = new HashMap<>();\n        final Map<String, CustomSpellSchool> spellSchoolTypes = new HashMap<>();\n        final Map<String, JsonNode> itemTypes = new HashMap<>();\n        final Map<String, JsonNode> itemProperties = new HashMap<>();\n        final Map<String, JsonNode> itemMastery = new HashMap<>();\n\n        HomebrewMetaTypes(Set<String> sourceKeys, String filename, JsonNode homebrewNode, String edition) {\n            this.primary = sourceKeys.iterator().next();\n            this.sourceKeys = sourceKeys;\n            this.filename = filename;\n            this.homebrewNode = homebrewNode;\n            this.edition = edition;\n        }\n\n        public String getOptionalFeatureType(String key) {\n            return optionalFeatureTypes.get(key.toLowerCase());\n        }\n\n        public void setOptionalFeatureType(String key, String value) {\n            optionalFeatureTypes.put(key.toLowerCase(), value);\n        }\n\n        public PsionicType getPsionicType(String key) {\n            return psionicTypes.get(key.toLowerCase());\n        }\n\n        public void setPsionicType(String key, JsonNode value) {\n            try {\n                CustomPsionicType psionicType = Tui.MAPPER.convertValue(value, CustomPsionicType.class);\n                psionicTypes.put(key.toLowerCase(), psionicType);\n            } catch (IllegalArgumentException e) {\n                Tui.instance().errorf(Msg.BREW, \"Error reading psionic type %s: %s\", key, value);\n            }\n        }\n\n        public SkillOrAbility getSkillType(String key) {\n            return skillOrAbility.get(key.toLowerCase());\n        }\n\n        public void setSkillType(String key, JsonNode skillNode) {\n            try {\n                CustomSkillOrAbility skill = new CustomSkillOrAbility(skillNode);\n                skillOrAbility.put(key.toLowerCase(), skill);\n            } catch (IllegalArgumentException e) {\n                Tui.instance().errorf(Msg.BREW, \"Error reading skill %s: %s\", key, skillNode);\n            }\n        }\n\n        public SpellSchool getSpellSchool(String key) {\n            return spellSchoolTypes.get(key.toLowerCase());\n        }\n\n        public void setSpellSchool(String key, JsonNode spellNode) {\n            try {\n                CustomSpellSchool school = new CustomSpellSchool(key,\n                        HomebrewFields.full.getTextOrEmpty(spellNode));\n                spellSchoolTypes.put(key.toLowerCase(), school);\n            } catch (IllegalArgumentException e) {\n                Tui.instance().errorf(Msg.BREW, \"Error reading skill %s: %s\", key, spellNode);\n            }\n        }\n\n        public JsonNode getItemType(String abbreviation) {\n            return itemTypes.get(abbreviation.toLowerCase());\n        }\n\n        public JsonNode getItemProperty(String abbreviation) {\n            return itemProperties.get(abbreviation.toLowerCase());\n        }\n\n        public JsonNode getItemMastery(String name) {\n            return itemMastery.get(name.toLowerCase());\n        }\n\n        public void addCrossReference(Tools5eIndexType type, String key, JsonNode value) {\n            String name = SourceField.name.getTextOrNull(value);\n            String abbreviation = Tools5eFields.abbreviation.getTextOrNull(value);\n\n            // Done before copies & variants are made\n            TtrpgValue.homebrewBaseSource.setIn(value, SourceField.source.getTextOrEmpty(value));\n            TtrpgValue.homebrewSource.setIn(value, SourceField.source.getTextOrEmpty(value));\n\n            if (isPresent(abbreviation)) {\n                // Make sure the key and sources have been constructed/assigned\n                if (type == Tools5eIndexType.itemType) {\n                    itemTypes.put(abbreviation.toLowerCase(), value);\n                } else if (type == Tools5eIndexType.itemProperty) {\n                    itemProperties.put(abbreviation.toLowerCase(), value);\n                }\n            } else if (isPresent(name)) {\n                if (type == Tools5eIndexType.itemMastery) {\n                    itemMastery.put(name.toLowerCase(), value);\n                } else if (type == Tools5eIndexType.skill) {\n                    setSkillType(name.toLowerCase(), value);\n                }\n            }\n        }\n    }\n\n    enum HomebrewFields implements JsonNodeReader {\n        abbreviation,\n        fonts,\n        full,\n        json,\n        optionalFeatureTypes,\n        psionicTypes,\n        skill,\n        sources,\n        spellSchools,\n        spellDistanceUnits\n    }\n\n    @Override\n    public CompendiumConfig cfg() {\n        return index.config;\n    }\n\n    @Override\n    public Tools5eIndex index() {\n        return index;\n    }\n\n    @Override\n    public Tools5eSources getSources() {\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/ItemMastery.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static dev.ebullient.convert.StringUtil.isPresent;\nimport static dev.ebullient.convert.StringUtil.toAnchorTag;\n\nimport java.util.Collection;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.config.TtrpgConfig;\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.tools.JsonTextConverter.SourceField;\nimport dev.ebullient.convert.tools.ToolsIndex.TtrpgValue;\n\npublic record ItemMastery(\n        String name,\n        String indexKey,\n        String tag) {\n\n    public String toString() {\n        return name + \" Mastery\";\n    }\n\n    public String linkify() {\n        return linkify(null);\n    }\n\n    public String linkify(String linkText) {\n        Tools5eIndex index = Tools5eIndex.instance();\n        linkText = isPresent(linkText) ? linkText : name;\n\n        boolean included = isPresent(indexKey)\n                ? index.isIncluded(indexKey)\n                : index.customContentIncluded();\n\n        if (!included) {\n            return linkText;\n        }\n        String path = TtrpgConfig.getConfig().splitRules()\n                ? \"item-mastery/item-mastery.md\"\n                : \"item-mastery.md\";\n        return \"[%s](%s%s#%s)\".formatted(\n                linkText, index.rulesVaultRoot(), path, toAnchorTag(name));\n    }\n\n    public static final Comparator<ItemMastery> comparator = Comparator.comparing(ItemMastery::name);\n    private static final Map<String, ItemMastery> masteryMap = new HashMap<>();\n\n    public static ItemMastery forKey(String key) {\n        if (!isPresent(key)) {\n            return null;\n        }\n        return masteryMap.get(key);\n    }\n\n    public static ItemMastery fromNode(JsonNode mastery) {\n        String key = TtrpgValue.indexKey.getTextOrEmpty(mastery);\n        // Create the ItemType object once\n        return masteryMap.computeIfAbsent(key, k -> {\n            String name = SourceField.name.getTextOrEmpty(mastery);\n            if (name == null) {\n                throw new IllegalArgumentException(\"Unable to get name for Mastery: \" + mastery.toPrettyString());\n            }\n\n            return new ItemMastery(\n                    name,\n                    key,\n                    \"item/mastery/\" + Tui.slugify(name));\n        });\n    }\n\n    public static List<String> asLinks(Collection<ItemMastery> itemMasteries) {\n        return itemMasteries.stream()\n                .map(ItemMastery::linkify)\n                .toList();\n    }\n\n    public static void clear() {\n        masteryMap.clear();\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/ItemProperty.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static dev.ebullient.convert.StringUtil.isPresent;\nimport static dev.ebullient.convert.StringUtil.toAnchorTag;\nimport static dev.ebullient.convert.StringUtil.valueOrDefault;\n\nimport java.util.Collection;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.config.TtrpgConfig;\nimport dev.ebullient.convert.io.Msg;\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.tools.JsonTextConverter.SourceField;\nimport dev.ebullient.convert.tools.ToolsIndex.TtrpgValue;\nimport dev.ebullient.convert.tools.dnd5e.JsonSource.Tools5eFields;\n\nrecord ItemProperty(\n        String name,\n        String abbreviation,\n        String indexKey,\n        String sectionName,\n        String tag) {\n\n    public String toString() {\n        return \"Property: \" + name;\n    }\n\n    public String linkify() {\n        return linkify(null);\n    }\n\n    public String linkify(String linkText) {\n        Tools5eIndex index = Tools5eIndex.instance();\n        linkText = isPresent(linkText) ? linkText : name;\n\n        boolean included = isPresent(indexKey)\n                ? index.isIncluded(indexKey)\n                : index.customContentIncluded();\n\n        return included\n                ? \"[%s](%sitem-properties.md#%s)\".formatted(\n                        linkText, index.rulesVaultRoot(),\n                        toAnchorTag(isPresent(sectionName) ? sectionName : name))\n                : linkText;\n    }\n\n    public static final Comparator<ItemProperty> comparator = Comparator.comparing(ItemProperty::name);\n    public static final Map<String, ItemProperty> propertyMap = new HashMap<>();\n\n    public static final ItemProperty CURSED = ItemProperty.customProperty(\"Cursed\", \"Cursed Items\", \"=\");\n    public static final ItemProperty SILVERED = ItemProperty.customProperty(\"Silvered\", \"Silvered Weapons\", \"=\");\n    public static final ItemProperty POISON = ItemProperty.customProperty(\"Poison\", \"=\");\n\n    public static ItemProperty forKey(String key) {\n        if (!isPresent(key)) {\n            return null;\n        }\n        return propertyMap.get(key);\n    }\n\n    public static ItemProperty fromNode(JsonNode property) {\n        if (property == null) {\n            return null;\n        }\n        String key = TtrpgValue.indexKey.getTextOrEmpty(property);\n        if (key.isEmpty()) {\n            Tui.instance().warnf(Msg.NOT_SET.wrap(\"Index key not found for property %s\"), property);\n            return null;\n        }\n        // Create the ItemType object once\n        return ItemProperty.propertyMap.computeIfAbsent(key, k -> {\n            String abbreviation = Tools5eFields.abbreviation.getTextOrEmpty(property);\n            String sectionName = null;\n\n            String name = SourceField.name.getTextOrNull(property);\n            if (name == null) {\n                JsonNode firstEntry = SourceField.entries.getFirstFromArray(property);\n                if (firstEntry == null) {\n                    firstEntry = Tools5eFields.entriesTemplate.getFirstFromArray(property);\n                }\n                if (firstEntry != null) {\n                    name = SourceField.name.getTextOrNull(firstEntry);\n                }\n                if (name == null) {\n                    Tui.instance().warnf(Msg.NOT_SET.wrap(\"Name not found for property %s\"), key);\n                    name = abbreviation;\n                }\n                // we've fished it out. remember it.\n                SourceField.name.setIn(property, name);\n            } else if (\"AF\".equals(abbreviation)) {\n                // Special case for firearms to distinguish from regular ammunition\n                name = \"Ammunition (Firearm)\";\n            } else if (\"S\".equals(abbreviation)) {\n                // Special, as the name, doesn't match the property/rules section\n                sectionName = \"Special Weapons\";\n            }\n\n            // item property tag w/ a few fixes\n            String tag = ItemTag.property.build(name\n                    .toLowerCase()\n                    .replace(\"extended reach\", \"reach/extended\"));\n\n            return new ItemProperty(\n                    name,\n                    abbreviation,\n                    key,\n                    sectionName,\n                    tag);\n        });\n    }\n\n    public static List<String> asLinks(Collection<ItemProperty> properties) {\n        return properties.stream()\n                .map(ItemProperty::linkify)\n                .toList();\n    }\n\n    /**\n     * Invented properties. No relevance to source material, but useful for\n     * links to rules, e.g. Poison.\n     *\n     * @param name\n     * @param sectionName Section heading in rules\n     * @param abbreviation\n     */\n    public static ItemProperty customProperty(String name, String sectionName, String abbreviation) {\n        return propertyMap.computeIfAbsent(sectionName, k -> {\n            return new ItemProperty(\n                    name,\n                    abbreviation,\n                    \"\",\n                    sectionName,\n                    ItemTag.property.build(name));\n        });\n    }\n\n    /**\n     * Invented properties. No relevance to source material, but useful for\n     * links to rules, e.g. Poison.\n     *\n     * @param name\n     * @param abbreviation\n     * @return\n     */\n    public static ItemProperty customProperty(String name, String abbreviation) {\n        return ItemProperty.customProperty(name, name, abbreviation);\n    }\n\n    public static void clear() {\n        propertyMap.clear();\n    }\n\n    public static String refTagToKey(String text) {\n        String[] parts = text.split(\"\\\\|\");\n        String abv = parts[0].trim();\n        String source = defaultItemSource(abv,\n                valueOrDefault(parts, 1, \"PHB\"));\n\n        return Tools5eIndexType.itemProperty.createKey(abv, source);\n    }\n\n    private static String defaultItemSource(String code, String source) {\n        boolean xphbAvailable = TtrpgConfig.getConfig().sourceIncluded(\"XPHB\") || Tools5eSources.has2024basicSrd();\n        boolean xdmgAvailable = TtrpgConfig.getConfig().sourceIncluded(\"XDMG\") || Tools5eSources.has2024basicSrd();\n        // reprints work mostly, but a few changed default between phb and dmg\n        return switch (code.toUpperCase()) {\n            // PHB <-> XPHB\n            case \"2H\", // two-handed\n                    \"A\", // ammunition\n                    \"F\", // finesse\n                    \"H\", // heavy\n                    \"L\", // light\n                    \"LD\", // loading\n                    \"R\", // reach\n                    \"T\", // thrown\n                    \"V\" // versatile\n                -> xphbAvailable ? \"XPHB\" : \"PHB\";\n\n            // DMG <-> XDMG\n            case \"AF\", // ammunition (futuristic)\n                    \"BF\", // burst fire\n                    \"RLD\" // reload\n                -> xdmgAvailable ? \"XDMG\" : \"DMG\";\n\n            case \"ER\" -> \"TDCSR\"; // extended reach\n            case \"S\" -> \"PHB\"; // special\n            case \"VST\" -> \"TDCSR\"; // vestige of divergence\n            default -> source;\n        };\n    }\n}\n\n// Parser.ITM_PROP_ABV__TWO_HANDED = \"2H\";\n// Parser.ITM_PROP_ABV__AMMUNITION = \"A\";\n// Parser.ITM_PROP_ABV__AMMUNITION_FUTURISTIC = \"AF\";\n// Parser.ITM_PROP_ABV__BURST_FIRE = \"BF\";\n// Parser.ITM_PROP_ABV__EXTENDED_REACH = \"ER\";\n// Parser.ITM_PROP_ABV__FINESSE = \"F\";\n// Parser.ITM_PROP_ABV__HEAVY = \"H\";\n// Parser.ITM_PROP_ABV__LIGHT = \"L\";\n// Parser.ITM_PROP_ABV__LOADING = \"LD\";\n// Parser.ITM_PROP_ABV__OTHER = \"OTH\";\n// Parser.ITM_PROP_ABV__REACH = \"R\";\n// Parser.ITM_PROP_ABV__RELOAD = \"RLD\";\n// Parser.ITM_PROP_ABV__SPECIAL = \"S\";\n// Parser.ITM_PROP_ABV__THROWN = \"T\";\n// Parser.ITM_PROP_ABV__VERSATILE = \"V\";\n// Parser.ITM_PROP_ABV__VESTIGE_OF_DIVERGENCE = \"Vst\";\n\n// Parser.ITM_PROP__TWO_HANDED = \"2H\";\n// Parser.ITM_PROP__AMMUNITION = \"A\";\n// Parser.ITM_PROP__AMMUNITION_FUTURISTIC = \"AF|DMG\";\n// Parser.ITM_PROP__BURST_FIRE = \"BF|DMG\";\n// Parser.ITM_PROP__EXTENDED_REACH = \"ER|TDCSR\";\n// Parser.ITM_PROP__FINESSE = \"F\";\n// Parser.ITM_PROP__HEAVY = \"H\";\n// Parser.ITM_PROP__LIGHT = \"L\";\n// Parser.ITM_PROP__LOADING = \"LD\";\n// Parser.ITM_PROP__OTHER = \"OTH\";\n// Parser.ITM_PROP__REACH = \"R\";\n// Parser.ITM_PROP__RELOAD = \"RLD|DMG\";\n// Parser.ITM_PROP__SPECIAL = \"S\";\n// Parser.ITM_PROP__THROWN = \"T\";\n// Parser.ITM_PROP__VERSATILE = \"V\";\n// Parser.ITM_PROP__VESTIGE_OF_DIVERGENCE = \"Vst|TDCSR\";\n\n// Parser.ITM_PROP__ODND_TWO_HANDED = \"2H|XPHB\";\n// Parser.ITM_PROP__ODND_AMMUNITION = \"A|XPHB\";\n// Parser.ITM_PROP__ODND_FINESSE = \"F|XPHB\";\n// Parser.ITM_PROP__ODND_HEAVY = \"H|XPHB\";\n// Parser.ITM_PROP__ODND_LIGHT = \"L|XPHB\";\n// Parser.ITM_PROP__ODND_LOADING = \"LD|XPHB\";\n// Parser.ITM_PROP__ODND_REACH = \"R|XPHB\";\n// Parser.ITM_PROP__ODND_THROWN = \"T|XPHB\";\n// Parser.ITM_PROP__ODND_VERSATILE = \"V|XPHB\";\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/ItemTag.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport java.util.Arrays;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.tools.Tags;\n\nenum ItemTag {\n    age,\n    armor,\n    attunement,\n    gear,\n    mastery,\n    property,\n    rarity,\n    shield,\n    tier,\n    vehicle,\n    weapon,\n    wondrous,\n    ;\n\n    void add(Tags tags, String... segments) {\n        tags.addRaw(build(segments));\n    }\n\n    String build(String... segments) {\n        return Stream.concat(Stream.of(\"item\", name()), Arrays.stream(segments))\n                .map(Tui::slugify)\n                .collect(Collectors.joining(\"/\"));\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/ItemType.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static dev.ebullient.convert.StringUtil.isPresent;\nimport static dev.ebullient.convert.StringUtil.toAnchorTag;\nimport static dev.ebullient.convert.StringUtil.valueOrDefault;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.config.TtrpgConfig;\nimport dev.ebullient.convert.io.Msg;\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.tools.JsonTextConverter.SourceField;\nimport dev.ebullient.convert.tools.ToolsIndex.TtrpgValue;\nimport dev.ebullient.convert.tools.dnd5e.JsonSource.Tools5eFields;\n\n/**\n * @param name Item type name.\n * @param lowercaseName Item type name in lowercase (for comparison)\n * @param abbreviation Item type abbreviation.\n * @param link Markdown link to definition of this type or name if source is not included.\n * @param group Item type group. Optional.\n */\npublic record ItemType(\n        String name,\n        String lowercaseName,\n        String abbreviation,\n        String indexKey,\n        ItemTypeGroup group) {\n\n    public String toString() {\n        return \"Type: \" + name;\n    }\n\n    public String linkify() {\n        return linkify(null);\n    }\n\n    public String linkify(String linkText) {\n        Tools5eIndex index = Tools5eIndex.instance();\n        linkText = isPresent(linkText) ? linkText : name;\n\n        boolean included = isPresent(indexKey)\n                ? index.isIncluded(indexKey)\n                : index.customContentIncluded();\n\n        return included\n                ? \"[%s](%sitem-types.md#%s)\".formatted(\n                        linkText, index.rulesVaultRoot(), toAnchorTag(name))\n                : linkText;\n    }\n\n    public static final Map<String, ItemType> typeMap = new HashMap<>();\n\n    public static ItemType forKey(String key) {\n        if (!isPresent(key)) {\n            return null;\n        }\n        return typeMap.get(key);\n    }\n\n    public static ItemType fromNode(JsonNode typeNode) {\n        if (typeNode == null) {\n            return null;\n        }\n        String typeKey = TtrpgValue.indexKey.getTextOrEmpty(typeNode);\n        if (typeKey.isEmpty()) {\n            Tui.instance().warnf(Msg.NOT_SET.wrap(\"Index key not found for type %s\"), typeNode);\n            return null;\n        }\n        // Create the ItemType object once\n        return typeMap.computeIfAbsent(typeKey, k -> {\n            String abbreviation = Tools5eFields.abbreviation.getTextOrEmpty(typeNode);\n            String name = SourceField.name.getTextOrEmpty(typeNode);\n            if (!isPresent(name)) {\n                Tui.instance().warnf(Msg.NOT_SET.wrap(\"Name not found for type %s\"), typeKey);\n                name = abbreviation;\n            }\n            name = fixName(abbreviation, name);\n            String lower = name.toLowerCase();\n            ItemTypeGroup group = mapGroup(abbreviation, lower, typeNode);\n\n            return new ItemType(\n                    name,\n                    lower,\n                    abbreviation,\n                    typeKey,\n                    group);\n        });\n    }\n\n    public static String tagForType(ItemType type, Tui tui) {\n        String lower = type.lowercaseName();\n        if (type.group() == ItemTypeGroup.armor) {\n            return ItemTag.armor.build(lower.replaceAll(\"\\\\s*armor\\\\s*\", \"\"));\n        }\n        if (type.group() == ItemTypeGroup.shield) {\n            return ItemTag.shield.build(lower.replaceAll(\"\\\\s*shield\\\\s*\", \"\"));\n        }\n        if (type.group() == ItemTypeGroup.vehicle) {\n            return ItemTag.vehicle.build(lower.replaceAll(\"\\\\s*vehicle\\\\s*\", \"\"));\n        }\n        if (type.group() == ItemTypeGroup.wondrous) {\n            return ItemTag.wondrous.build(lower.replaceAll(\"\\\\s*wondrous( item)?\\\\s*\", \"\"));\n        }\n        if (type.group() == ItemTypeGroup.weapon) {\n            return ItemTag.weapon.build(\"\"\n                    + (lower.contains(\"firearm\") ? \"firearm/\" : \"\")\n                    + (lower.contains(\"ammunition\") || lower.contains(\"ammo\") ? \"ammunition/\" : \"\")\n                    + (lower.contains(\"explosive\") ? \"explosive/\" : \"\")\n                    + lower.replaceAll(\"\\\\s*(ammo|ammunition|explosive|firearm|weapon)\\\\s*\", \"\"));\n        }\n        return ItemTag.gear.build(lower.replaceAll(\"\\\\s*(adventuring|gear)\\\\s*\", \"\"));\n    }\n\n    private static String fixName(String abbreviation, String name) {\n        return switch (abbreviation) {\n            case \"AF\" -> \"Ammunition (Firearm)\";\n            case \"AIR\" -> \"Airship, Vehicle (air)\";\n            case \"SHP\" -> \"Ship, Vehicle (water)\";\n            case \"SPC\" -> \"Spelljammer, Vehicle (space)\";\n            case \"VEH\" -> \"Vehicle (land)\";\n            default -> name;\n        };\n    }\n\n    private static ItemTypeGroup mapGroup(String abbreviation, String lowercase, JsonNode itemType) {\n        if (abbreviation.contains(\"$\") || lowercase.contains(\"treasure\")) {\n            return ItemTypeGroup.treasure;\n        }\n        if (lowercase.contains(\"ammunition\")) {\n            return ItemTypeGroup.ammunition;\n        }\n        if (lowercase.contains(\"armor\")) {\n            return ItemTypeGroup.armor;\n        }\n        if (lowercase.contains(\"shield\")) {\n            return ItemTypeGroup.shield;\n        }\n        if (lowercase.contains(\"vehicle\")) {\n            return ItemTypeGroup.vehicle;\n        }\n        if (lowercase.contains(\"weapon\")\n                || lowercase.contains(\"explosive\")\n                || lowercase.contains(\"firearm\")) {\n            return ItemTypeGroup.weapon;\n        }\n        if (lowercase.contains(\"wondrous\")) {\n            return ItemTypeGroup.wondrous;\n        }\n        return switch (abbreviation) {\n            // generic variant, other\n            // potion, rod, ring, scroll, staff, wand, wondrous\n            case \"GV\", \"MR\", \"OTH\", \"P\", \"RD\", \"RG\", \"SC\", \"ST\", \"WD\", \"W\" -> ItemTypeGroup.wondrous;\n            // artisan tool, food & drink, gear, gaming set, illegal drug, musical instrument,\n            // mount, spellcasting focus, tools, tack & harness, trade good,\n            case \"AT\", \"FD\", \"G\", \"GS\", \"IDG\", \"INS\", \"MNT\", \"SCF\", \"T\", \"TAH\", \"TG\" -> ItemTypeGroup.gear;\n            default -> {\n                // Homebrew won't always have a type assigned. Poke around.\n                String nodeString = itemType.toString();\n                if (nodeString.contains(\"this weapon\")) {\n                    yield ItemTypeGroup.weapon;\n                }\n                if (lowercase.contains(\"magic\")\n                        || lowercase.contains(\"rune\")\n                        || nodeString.contains(\"magic\")) {\n                    yield ItemTypeGroup.wondrous;\n                }\n                yield ItemTypeGroup.gear;\n            }\n        };\n    }\n\n    public static void clear() {\n        typeMap.clear();\n    }\n\n    public static String refTagToKey(String text) {\n        String[] parts = text.split(\"\\\\|\");\n        String abv = parts[0].trim();\n        String source = defaultItemSource(abv,\n                valueOrDefault(parts, 1, \"PHB\"));\n\n        return Tools5eIndexType.itemType.createKey(abv, source);\n    }\n\n    private static String defaultItemSource(String code, String source) {\n        boolean xphbAvailable = TtrpgConfig.getConfig().sourceIncluded(\"XPHB\") || Tools5eSources.has2024basicSrd();\n        boolean xdmgAvailable = TtrpgConfig.getConfig().sourceIncluded(\"XDMG\") || Tools5eSources.has2024basicSrd();\n        // reprints work mostly, but a few changed default between phb and dmg\n        return switch (code.toUpperCase()) {\n            // PHB <-> XPHB\n            case \"$C\", // coinage\n                    \"A\", // ammunition\n                    \"AT\", // artisan tool\n                    \"FD\", // food & drink\n                    \"G\", // adventuring gear\n                    \"GS\", // gaming set\n                    \"HA\", // heavy armor\n                    \"INS\", // instrument\n                    \"LA\", // light armor\n                    \"M\", // melee weapon\n                    \"MA\", // medium armor\n                    \"MNT\", // mount\n                    \"P\", // potion\n                    \"R\", // ranged weapon\n                    \"S\", // shield\n                    \"SCF\", // spellcasting focus\n                    \"T\", // tool\n                    \"TAH\", // tack & harness\n                    \"VEH\" // vehicle (land)\n                -> xphbAvailable ? \"XPHB\" : \"PHB\";\n\n            // PHB <-> XDMG\n            case \"TG\" // trade good\n                -> xdmgAvailable ? \"XDMG\" : \"PHB\";\n\n            // DMG <-> XDMG\n            case \"$A\", // art object\n                    \"$G\", // gemstone\n                    \"AF\", // ammunition (futuristic)\n                    \"EXP\" // explosive\n                -> xdmgAvailable ? \"XDMG\" : \"DMG\";\n\n            // DMG <-> XPHB\n            case \"AIR\", // air vehicle\n                    \"SC\", // scroll\n                    \"SHP\" // ship (water)\n                -> xphbAvailable ? \"XPHB\" : \"DMG\";\n\n            case \"$\", // treasure\n                    \"GV\", // generic variant\n                    \"RD\", // rod\n                    \"RG\", // ring\n                    \"WD\" // wand\n                -> \"DMG\"; //  (2014 only)\n\n            case \"IDG\" -> \"TDCSR\"; // illegal drug\n            case \"OTH\" -> \"PHB\"; // other (2014 only)\n            case \"SPC\" -> \"AAG\"; // spelljammer\n            case \"TB\" -> \"XDMG\"; // trade bar (2024 only)\n            default -> source;\n        };\n    }\n}\n\n// Parser.ITM_TYP_ABV__TREASURE = \"$\";\n// Parser.ITM_TYP_ABV__TREASURE_ART_OBJECT = \"$A\";\n// Parser.ITM_TYP_ABV__TREASURE_COINAGE = \"$C\";\n// Parser.ITM_TYP_ABV__TREASURE_GEMSTONE = \"$G\";\n// Parser.ITM_TYP_ABV__AMMUNITION = \"A\";\n// Parser.ITM_TYP_ABV__AMMUNITION_FUTURISTIC = \"AF\";\n// Parser.ITM_TYP_ABV__VEHICLE_AIR = \"AIR\";\n// Parser.ITM_TYP_ABV__ARTISAN_TOOL = \"AT\";\n// Parser.ITM_TYP_ABV__EXPLOSIVE = \"EXP\";\n// Parser.ITM_TYP_ABV__FOOD_AND_DRINK = \"FD\";\n// Parser.ITM_TYP_ABV__ADVENTURING_GEAR = \"G\";\n// Parser.ITM_TYP_ABV__GAMING_SET = \"GS\";\n// Parser.ITM_TYP_ABV__GENERIC_VARIANT = \"GV\";\n// Parser.ITM_TYP_ABV__HEAVY_ARMOR = \"HA\";\n// Parser.ITM_TYP_ABV__ILLEGAL_DRUG = \"IDG\";\n// Parser.ITM_TYP_ABV__INSTRUMENT = \"INS\";\n// Parser.ITM_TYP_ABV__LIGHT_ARMOR = \"LA\";\n// Parser.ITM_TYP_ABV__MELEE_WEAPON = \"M\";\n// Parser.ITM_TYP_ABV__MEDIUM_ARMOR = \"MA\";\n// Parser.ITM_TYP_ABV__MOUNT = \"MNT\";\n// Parser.ITM_TYP_ABV__OTHER = \"OTH\";\n// Parser.ITM_TYP_ABV__POTION = \"P\";\n// Parser.ITM_TYP_ABV__RANGED_WEAPON = \"R\";\n// Parser.ITM_TYP_ABV__ROD = \"RD\";\n// Parser.ITM_TYP_ABV__RING = \"RG\";\n// Parser.ITM_TYP_ABV__SHIELD = \"S\";\n// Parser.ITM_TYP_ABV__SCROLL = \"SC\";\n// Parser.ITM_TYP_ABV__SPELLCASTING_FOCUS = \"SCF\";\n// Parser.ITM_TYP_ABV__VEHICLE_WATER = \"SHP\";\n// Parser.ITM_TYP_ABV__VEHICLE_SPACE = \"SPC\";\n// Parser.ITM_TYP_ABV__TOOL = \"T\";\n// Parser.ITM_TYP_ABV__TACK_AND_HARNESS = \"TAH\";\n// Parser.ITM_TYP_ABV__TRADE_BAR = \"TB\";\n// Parser.ITM_TYP_ABV__TRADE_GOOD = \"TG\";\n// Parser.ITM_TYP_ABV__VEHICLE_LAND = \"VEH\";\n// Parser.ITM_TYP_ABV__WAND = \"WD\";\n\n// Parser.ITM_TYP__TREASURE = \"$|DMG\";\n// Parser.ITM_TYP__TREASURE_ART_OBJECT = \"$A|DMG\";\n// Parser.ITM_TYP__TREASURE_COINAGE = \"$C\";\n// Parser.ITM_TYP__TREASURE_GEMSTONE = \"$G|DMG\";\n// Parser.ITM_TYP__AMMUNITION = \"A\";\n// Parser.ITM_TYP__AMMUNITION_FUTURISTIC = \"AF|DMG\";\n// Parser.ITM_TYP__VEHICLE_AIR = \"AIR|DMG\";\n// Parser.ITM_TYP__ARTISAN_TOOL = \"AT\";\n// Parser.ITM_TYP__EXPLOSIVE = \"EXP|DMG\";\n// Parser.ITM_TYP__FOOD_AND_DRINK = \"FD\";\n// Parser.ITM_TYP__ADVENTURING_GEAR = \"G\";\n// Parser.ITM_TYP__GAMING_SET = \"GS\";\n// Parser.ITM_TYP__GENERIC_VARIANT = \"GV|DMG\";\n// Parser.ITM_TYP__HEAVY_ARMOR = \"HA\";\n// Parser.ITM_TYP__ILLEGAL_DRUG = \"IDG|TDCSR\";\n// Parser.ITM_TYP__INSTRUMENT = \"INS\";\n// Parser.ITM_TYP__LIGHT_ARMOR = \"LA\";\n// Parser.ITM_TYP__MELEE_WEAPON = \"M\";\n// Parser.ITM_TYP__MEDIUM_ARMOR = \"MA\";\n// Parser.ITM_TYP__MOUNT = \"MNT\";\n// Parser.ITM_TYP__OTHER = \"OTH\";\n// Parser.ITM_TYP__POTION = \"P\";\n// Parser.ITM_TYP__RANGED_WEAPON = \"R\";\n// Parser.ITM_TYP__ROD = \"RD|DMG\";\n// Parser.ITM_TYP__RING = \"RG|DMG\";\n// Parser.ITM_TYP__SHIELD = \"S\";\n// Parser.ITM_TYP__SCROLL = \"SC|DMG\";\n// Parser.ITM_TYP__SPELLCASTING_FOCUS = \"SCF\";\n// Parser.ITM_TYP__VEHICLE_WATER = \"SHP\";\n// Parser.ITM_TYP__VEHICLE_SPACE = \"SPC|AAG\";\n// Parser.ITM_TYP__TOOL = \"T\";\n// Parser.ITM_TYP__TACK_AND_HARNESS = \"TAH\";\n// Parser.ITM_TYP__TRADE_GOOD = \"TG\";\n// Parser.ITM_TYP__VEHICLE_LAND = \"VEH\";\n// Parser.ITM_TYP__WAND = \"WD|DMG\";\n\n// Parser.ITM_TYP__ODND_TREASURE_ART_OBJECT = \"$A|XDMG\";\n// Parser.ITM_TYP__ODND_TREASURE_COINAGE = \"$C|XPHB\";\n// Parser.ITM_TYP__ODND_TREASURE_GEMSTONE = \"$G|XDMG\";\n// Parser.ITM_TYP__ODND_AMMUNITION = \"A|XPHB\";\n// Parser.ITM_TYP__ODND_AMMUNITION_FUTURISTIC = \"AF|XDMG\";\n// Parser.ITM_TYP__ODND_VEHICLE_AIR = \"AIR|XPHB\";\n// Parser.ITM_TYP__ODND_ARTISAN_TOOL = \"AT|XPHB\";\n// Parser.ITM_TYP__ODND_EXPLOSIVE = \"EXP|XDMG\";\n// Parser.ITM_TYP__ODND_FOOD_AND_DRINK = \"FD|XPHB\";\n// Parser.ITM_TYP__ODND_ADVENTURING_GEAR = \"G|XPHB\";\n// Parser.ITM_TYP__ODND_GAMING_SET = \"GS|XPHB\";\n// Parser.ITM_TYP__ODND_GENERIC_VARIANT = \"GV|XDMG\";\n// Parser.ITM_TYP__ODND_HEAVY_ARMOR = \"HA|XPHB\";\n// Parser.ITM_TYP__ODND_INSTRUMENT = \"INS|XPHB\";\n// Parser.ITM_TYP__ODND_LIGHT_ARMOR = \"LA|XPHB\";\n// Parser.ITM_TYP__ODND_MELEE_WEAPON = \"M|XPHB\";\n// Parser.ITM_TYP__ODND_MEDIUM_ARMOR = \"MA|XPHB\";\n// Parser.ITM_TYP__ODND_MOUNT = \"MNT|XPHB\";\n// Parser.ITM_TYP__ODND_POTION = \"P|XPHB\";\n// Parser.ITM_TYP__ODND_RANGED_WEAPON = \"R|XPHB\";\n// Parser.ITM_TYP__ODND_ROD = \"RD|XDMG\";\n// Parser.ITM_TYP__ODND_RING = \"RG|XDMG\";\n// Parser.ITM_TYP__ODND_SHIELD = \"S|XPHB\";\n// Parser.ITM_TYP__ODND_SCROLL = \"SC|XPHB\";\n// Parser.ITM_TYP__ODND_SPELLCASTING_FOCUS = \"SCF|XPHB\";\n// Parser.ITM_TYP__ODND_VEHICLE_WATER = \"SHP|XPHB\";\n// Parser.ITM_TYP__ODND_TOOL = \"T|XPHB\";\n// Parser.ITM_TYP__ODND_TACK_AND_HARNESS = \"TAH|XPHB\";\n// Parser.ITM_TYP__ODND_TRADE_BAR = \"TB|XDMG\";\n// Parser.ITM_TYP__ODND_TRADE_GOOD = \"TG|XDMG\";\n// Parser.ITM_TYP__ODND_VEHICLE_LAND = \"VEH|XPHB\";\n// Parser.ITM_TYP__ODND_WAND = \"WD|XDMG\";\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/ItemTypeGroup.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport io.quarkus.qute.TemplateData;\n\n@TemplateData\npublic enum ItemTypeGroup {\n    ammunition,\n    armor,\n    gear,\n    shield,\n    treasure,\n    vehicle,\n    weapon,\n    wondrous;\n\n    public boolean hasGroup(ItemType type, ItemType typeAlt) {\n        return (type != null && this == type.group())\n                || (typeAlt != null && this == typeAlt.group());\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteBackground.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.Json2QuteFeat.FeatFields;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteBackground;\nimport dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase;\n\npublic class Json2QuteBackground extends Json2QuteCommon {\n\n    public static final Set<String> traits = new HashSet<>();\n    public static final Set<String> ideals = new HashSet<>();\n    public static final Set<String> bonds = new HashSet<>();\n    public static final Set<String> flaws = new HashSet<>();\n\n    final String backgroundName;\n\n    Json2QuteBackground(Tools5eIndex index, Tools5eIndexType type, JsonNode jsonNode) {\n        super(index, type, jsonNode);\n        backgroundName = linkifier().decoratedName(type, jsonNode);\n    }\n\n    @Override\n    protected Tools5eQuteBase buildQuteResource() {\n        Tags tags = new Tags(getSources());\n        tags.add(\"background\");\n\n        List<String> text = new ArrayList<>();\n        appendToText(text, rootNode, \"##\");\n\n        List<ImageRef> images = new ArrayList<>();\n        List<String> fluff = getFluff(Tools5eIndexType.backgroundFluff, \"##\", images);\n\n        if (fluff != null) {\n            boolean found = false;\n            for (int i = 0; i < text.size(); i++) {\n                if (text.get(i).startsWith(\"##\")) {\n                    found = true;\n                    text.add(i, \"\");\n                    text.addAll(i, fluff);\n                    break;\n                }\n            }\n            if (!found) {\n                maybeAddBlankLine(text);\n                text.addAll(fluff);\n            }\n        }\n        return new QuteBackground(sources,\n                backgroundName,\n                getSourceText(sources),\n                listPrerequisites(rootNode),\n                SkillOrAbility.getAbilityScoreIncreases(FeatFields.ability.getFrom(rootNode)),\n                images,\n                String.join(\"\\n\", text),\n                tags);\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteBastion.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static dev.ebullient.convert.StringUtil.isPresent;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.io.Msg;\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.tools.JsonNodeReader;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteBastion;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteBastion.Hireling;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteBastion.Space;\nimport dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase;\n\npublic class Json2QuteBastion extends Json2QuteCommon {\n\n    Map<String, Space> spaceMap = new HashMap<>();\n\n    Json2QuteBastion(Tools5eIndex index, Tools5eIndexType type, JsonNode jsonNode) {\n        super(index, type, jsonNode);\n    }\n\n    @Override\n    protected Tools5eQuteBase buildQuteResource() {\n        Tags tags = new Tags(getSources());\n        tags.add(\"bastion\");\n\n        List<ImageRef> fluffImages = new ArrayList<>();\n        List<String> text = getFluff(Tools5eIndexType.facilityFluff, \"##\", fluffImages);\n        appendToText(text, rootNode, \"##\");\n\n        String type = BastionFields.facilityType.getTextOrThrow(rootNode);\n\n        String prereqs = \"\";\n        if (BastionFields.prerequisite.existsIn(rootNode)) {\n            prereqs = listPrerequisites(rootNode);\n        } else if (!\"basic\".equals(type)) {\n            prereqs = \"None\";\n        }\n\n        List<Hireling> hirelings = new ArrayList<>();\n        for (JsonNode h : BastionFields.hirelings.iterateArrayFrom(rootNode)) {\n            hirelings.add(new Hireling(\n                    BastionFields.exact.intOrNull(h),\n                    BastionFields.min.intOrNull(h),\n                    BastionFields.max.intOrNull(h),\n                    spaceForName(BastionFields.space.getTextOrEmpty(h))));\n        }\n\n        List<Space> spaces = new ArrayList<>();\n        for (JsonNode s : BastionFields.space.iterateArrayFrom(rootNode)) {\n            Space space = spaceForName(s.asText());\n            if (space == null) {\n                // TODO: At some point, there will be a custom bastion space..\n                tui().warnf(Msg.UNRESOLVED, \"Bastion space %s not found (%s)\", s, getSources().getKey());\n            } else {\n                spaces.add(space);\n            }\n        }\n\n        return new QuteBastion(\n                sources,\n                getName(),\n                getSourceText(getSources()),\n                hirelings,\n                BastionFields.level.getTextOrEmpty(rootNode),\n                BastionFields.orders.getListOfStrings(rootNode, tui()),\n                prereqs,\n                spaces,\n                type,\n                String.join(\"\\n\", text),\n                fluffImages,\n                tags);\n    }\n\n    private Space spaceForName(String name) {\n        if (!isPresent(name)) {\n            return null;\n        }\n        if (spaceMap.isEmpty()) {\n            Space cramped = new Space(\"Cramped\", 4, 500, 20);\n            Space roomy = new Space(\"Roomy\", 16, 1000, 45, cramped);\n            Space vast = new Space(\"Vast\", 36, 3000, 125, roomy);\n            spaceMap.put(\"cramped\", cramped);\n            spaceMap.put(\"roomy\", roomy);\n            spaceMap.put(\"vast\", vast);\n        }\n        return spaceMap.get(name.toLowerCase());\n    }\n\n    enum BastionFields implements JsonNodeReader {\n        exact,\n        facilityType,\n        hirelings,\n        level,\n        min,\n        max,\n        orders,\n        space,\n        prerequisite,\n        fluffImages;\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteBook.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicInteger;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteNote;\n\npublic class Json2QuteBook extends Json2QuteCommon {\n\n    final String bookRelativePath;\n    final JsonNode dataNode;\n    final String title;\n    String fileName;\n\n    public Json2QuteBook(Tools5eIndex index, Tools5eIndexType type, JsonNode rootNode, JsonNode dataNode) {\n        super(index, type, rootNode);\n        this.dataNode = dataNode;\n        this.bookRelativePath = slugify(sources.getName());\n        this.title = replaceText(sources.getName());\n\n        String key = getSources().getKey();\n        final String basePath;\n        if (key.contains(\"adventure-\") || key.contains(\"book-\")) {\n            basePath = linkifier().getRelativePath(type);\n        } else {\n            basePath = \".\";\n        }\n        imagePath = basePath + \"/\" + bookRelativePath;\n    }\n\n    @Override\n    public String getName() {\n        if (fileName != null) {\n            return fileName;\n        }\n        return title;\n    }\n\n    /**\n     * From index entry and supporting data, construct a set of pages for the book.\n     * Page state has to be maintained.\n     */\n    public List<Tools5eQuteNote> buildBook() {\n        List<Tools5eQuteNote> pages = new ArrayList<>();\n        Tags tags = new Tags(getSources());\n        JsonNode data = dataNode.get(\"data\");\n\n        AtomicInteger prefix = new AtomicInteger(1);\n        final String pFormat;\n        if (data.size() + 1 > 10) {\n            pFormat = \"%02d\";\n        } else {\n            pFormat = \"%01d\";\n        }\n\n        boolean p1 = parseState().push(getSources()); // set source\n        try {\n            for (JsonNode x : iterableElements(data)) {\n                boolean p2 = parseState().push(x); // inner node\n                try {\n                    String name = replaceText(SourceField.name.getTextOrEmpty(x));\n                    fileName = String.format(\"%s-%s\",\n                            String.format(pFormat, prefix.get()),\n                            slugify(name));\n\n                    List<String> text = new ArrayList<>();\n                    appendToText(text, SourceField.entries.getFrom(x), \"##\");\n\n                    String content = String.join(\"\\n\", text);\n\n                    if (!content.isBlank()) {\n                        String titlePage = title;\n                        if (x.has(\"page\")) {\n                            String page = x.get(\"page\").asText();\n                            titlePage = title + \", p. \" + page;\n                        }\n                        Tools5eQuteNote note = new Tools5eQuteNote(getSources(), name, titlePage, content, tags)\n                                .withTargetPath(imagePath)\n                                .withTargetFile(fileName);\n                        pages.add(note);\n                        prefix.incrementAndGet();\n                    }\n                } finally {\n                    parseState().pop(p2);\n                }\n            }\n        } finally {\n            parseState().pop(p1);\n        }\n        return pages;\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteClass.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static dev.ebullient.convert.StringUtil.isPresent;\nimport static dev.ebullient.convert.StringUtil.join;\nimport static dev.ebullient.convert.StringUtil.joinConjunct;\nimport static dev.ebullient.convert.StringUtil.markdownLinkToHtml;\nimport static dev.ebullient.convert.StringUtil.toAnchorTag;\nimport static dev.ebullient.convert.StringUtil.toOrdinal;\nimport static dev.ebullient.convert.StringUtil.toTitleCase;\nimport static dev.ebullient.convert.StringUtil.uppercaseFirst;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.io.Msg;\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.tools.JsonNodeReader;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.ToolsIndex.TtrpgValue;\nimport dev.ebullient.convert.tools.dnd5e.OptionalFeatureIndex.OptionalFeatureType;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteClass;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteClass.HitPointDie;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteClass.Multiclassing;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteClass.StartingEquipment;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteSubclass;\n\npublic class Json2QuteClass extends Json2QuteCommon {\n    final static Pattern footnotePattern = Pattern.compile(\"\\\\^\\\\[([^\\\\]]+)\\\\]\");\n\n    final static Map<String, ClassFeature> keyToClassFeature = new HashMap<>();\n\n    final Map<String, List<String>> startingText = new HashMap<>();\n    final boolean isSidekick;\n    final String decoratedClassName;\n    final String classSource;\n    final String subclassTitle;\n    final String primaryAbility;\n\n    String filename = null;\n    SidekickProficiencies sidekickProficiencies = null;\n\n    Json2QuteClass(Tools5eIndex index, Tools5eIndexType type, JsonNode jsonNode) {\n        super(index, type, jsonNode);\n        decoratedClassName = linkifier().decoratedName(type, jsonNode);\n        classSource = jsonNode.get(\"source\").asText();\n        isSidekick = ClassFields.isSidekick.booleanOrDefault(jsonNode, false);\n        subclassTitle = ClassFields.subclassTitle.getTextOrEmpty(jsonNode);\n        primaryAbility = buildPrimaryAbility();\n    }\n\n    @Override\n    public String getFileName() {\n        return filename == null\n                ? super.getFileName()\n                : filename;\n    }\n\n    @Override\n    protected QuteClass buildQuteResource() {\n        // Some compensation for sidekicks. Do this first\n        List<ClassFeature> features = findClassFeatures(\n                Tools5eIndexType.classfeature,\n                ClassFields.classFeatures.ensureArrayIn(rootNode),\n                ClassFields.classFeature);\n\n        Tags tags = new Tags(getSources());\n        tags.add(\"class\", getName());\n\n        List<String> progression = buildProgressionTable(features, rootNode, ClassFields.classTableGroups);\n\n        List<ImageRef> images = new ArrayList<>();\n        List<String> text = getFluff(Tools5eIndexType.classFluff, \"##\", images);\n\n        maybeAddBlankLine(text);\n        text.add(\"## Class Features\");\n        for (ClassFeature cf : features) {\n            cf.appendText(this, text, getSources().primarySource());\n        }\n\n        addOptionalFeatureText(rootNode, getSources().primarySource(), text);\n\n        return new QuteClass(getSources(),\n                decoratedClassName,\n                getSourceText(getSources()),\n                String.join(\"\\n\", progression),\n                primaryAbility,\n                buildHitDie(),\n                buildStartingEquipment(),\n                buildMulticlassing(),\n                String.join(\"\\n\", text),\n                images,\n                tags);\n    }\n\n    public List<QuteSubclass> buildSubclasses() {\n        List<QuteSubclass> quteSc = new ArrayList<>();\n\n        // List of subclasses may include duplicates\n        // See recovery for included subclass features that get abandoned\n        // over edition crossing\n        Set<String> subclassKeys = index.findSubclasses(getSources().getKey());\n        for (String scKey : subclassKeys) {\n            JsonNode scNode = index.getNode(scKey);\n            Tools5eSources scSources = Tools5eSources.findSources(scKey);\n            String scName = scSources.getName();\n            String scShortName = ClassFields.shortName.getTextOrDefault(scNode, scName);\n\n            List<ClassFeature> scFeatures = findClassFeatures(\n                    Tools5eIndexType.subclassFeature,\n                    ClassFields.subclassFeatures.ensureArrayIn(scNode),\n                    ClassFields.subclassFeature);\n\n            filename = linkifier().getTargetFileName(scName, scSources);\n            boolean pushed = parseState().push(scSources);\n            try {\n                Tags tags = new Tags(scSources);\n                tags.add(\"subclass\", getName(), scShortName);\n\n                if (getName().matches(\".*[Cc]leric.*\")) {\n                    tags.add(\"domain\", scShortName);\n                }\n\n                List<String> progression = buildProgressionTable(scFeatures, scNode, ClassFields.subclassTableGroups);\n\n                List<ImageRef> images = new ArrayList<>();\n                List<String> text = getFluff(scNode, Tools5eIndexType.subclassFluff, \"##\", images);\n\n                // A bit hacky, but the isClassic setting won't always be present for homebrew\n                // Homebrew based on the phb is considered classic\n                boolean scIsClassic = scSources.isClassic()\n                        || (TtrpgValue.isHomebrew.booleanOrDefault(scNode, false)\n                                && Tools5eFields.classSource.getTextOrDefault(scNode, \"phb\").equalsIgnoreCase(\"phb\"));\n\n                if (scIsClassic && !getSources().isClassic()) {\n                    // insert warning about mixed edition content\n                    text.add(0,\n                            \"> This subclass is from a different game edition. You will need to do some adjustment to resolve differences.\");\n                    text.add(0, \"> [!caution] Mixed edition content\");\n                }\n\n                maybeAddBlankLine(text);\n                for (ClassFeature scf : scFeatures) {\n                    if (scf.getName().equalsIgnoreCase(scName)) {\n                        // This \"feature\" is the flavor text for the subclass, so treat it like fluff text\n                        scf.appendIntroText(this, text, scSources.primarySource());\n                    } else {\n                        scf.appendText(this, text, scSources.primarySource());\n                    }\n                }\n\n                addOptionalFeatureText(scNode, scSources.primarySource(), text);\n\n                quteSc.add(new QuteSubclass(scSources,\n                        scName,\n                        getSourceText(scSources),\n                        getName(), // parentClassName\n                        String.format(\"[%s](./%s.md)\", decoratedClassName, // peer/sibling\n                                linkifier().getClassResource(getName(), getSources().primarySource())),\n                        getSources().primarySource(),\n                        subclassTitle,\n                        String.join(\"\\n\", progression),\n                        String.join(\"\\n\", text),\n                        images,\n                        tags));\n\n            } finally {\n                parseState().pop(pushed);\n                filename = null;\n            }\n        }\n        return quteSc;\n    }\n\n    private ClassFeature getClassFeature(String featureKey, Tools5eIndexType featureType) {\n        return keyToClassFeature.computeIfAbsent(featureKey, k -> {\n            JsonNode featureNode = index().getOriginNoFallback(featureKey);\n            return new ClassFeature(featureType, featureKey, featureNode);\n        });\n    }\n\n    List<ClassFeature> findClassFeatures(Tools5eIndexType featureType, JsonNode featureElements,\n            ClassFields field) {\n        List<ClassFeature> features = new ArrayList<>();\n        for (JsonNode featureNode : featureElements) {\n\n            String featureKey = featureNode.isTextual()\n                    ? featureType.fromTagReference(featureNode.asText())\n                    : featureType.fromTagReference(ClassFields.classFeature.getTextOrEmpty(featureNode));\n\n            var classFeature = getClassFeature(featureKey, featureType);\n\n            if (isSidekick && \"1\".equals(classFeature.level())\n                    && \"Bonus Proficiencies\".equalsIgnoreCase(classFeature.getName())) {\n                // sidekick classes don't have startingProficiencies, they have a bonus\n                // proficiency feature instead.\n                sidekickProficiencies = new SidekickProficiencies(classFeature.cfNode());\n            }\n            // Add to list of features (for content rendering later)\n            features.add(classFeature);\n        }\n        return features;\n    }\n\n    void addOptionalFeatureText(JsonNode optFeatures, String primarySource, List<String> text) {\n        JsonNode optionalFeatureProgression = ClassFields.optionalfeatureProgression.getFrom(optFeatures);\n        if (optionalFeatureProgression == null) {\n            return;\n        }\n\n        String relativePath = linkifier().getRelativePath(Tools5eIndexType.optionalFeatureTypes);\n\n        maybeAddBlankLine(text);\n        text.add(\"## Optional Features\");\n        for (JsonNode ofp : iterableElements(optionalFeatureProgression)) {\n            for (String featureType : Tools5eFields.featureType.getListOfStrings(ofp, tui())) {\n                OptionalFeatureType oft = index.getOptionalFeatureType(featureType);\n                if (oft != null) {\n                    maybeAddBlankLine(text);\n                    String title = oft.getTitle(); // this could be long if homebrew mixed\n                    text.add(\"> [!example]- Optional Features: \" + title);\n                    text.add(String.format(\"> ![%s](%s%s/%s.md#%s)\",\n                            title,\n                            index().compendiumVaultRoot(), relativePath,\n                            oft.getFilename(),\n                            toAnchorTag(title)));\n                    text.add(\"^list-optfeature-\" + slugify(oft.abbreviation));\n                } else {\n                    tui().errorf(\n                            Msg.UNRESOLVED, \"Can not find optional feature type %s for progression. Source: %s; Reference: %s\",\n                            featureType, ofp, parseState().getSource());\n                }\n            }\n        }\n    }\n\n    String buildPrimaryAbility() {\n        // primary ability only exists in 2024 class versions\n        if (!ClassFields.primaryAbility.existsIn(rootNode)) {\n            return null;\n        }\n\n        // Array of objects with multiple properties\n        List<String> abilities = ClassFields.primaryAbility.streamProps(rootNode)\n                .map(e -> {\n                    return streamPropsExcluding(rootNode)\n                            .filter(x -> x.getValue().asBoolean())\n                            .map(x -> SkillOrAbility.format(x.getKey(), index(), getSources()))\n                            .toList();\n                })\n                .map(l -> joinConjunct(\" and \", l))\n                .toList();\n        return joinConjunct(\" or \", abilities);\n    }\n\n    HitPointDie buildHitDie() {\n        if (isSidekick) {\n            // Sidekicks do not have a hit die. Hit points depend on creature statblock\n            return new HitPointDie(getName(), 0, 0, this.sources.isClassic(), isSidekick);\n        }\n        JsonNode hdNode = ClassFields.hd.getFrom(rootNode);\n        if (hdNode != null) {\n            // both attributes are required. Default should not be necessary\n            return new HitPointDie(\n                    getName(),\n                    ClassFields.number.intOrDefault(hdNode, 1),\n                    ClassFields.faces.intOrDefault(hdNode, 1),\n                    this.sources.isClassic(),\n                    isSidekick);\n        }\n        return null;\n    }\n\n    StartingEquipment buildStartingEquipment() {\n        if (isSidekick) {\n            if (sidekickProficiencies == null) {\n                tui().warnf(Msg.UNKNOWN, \"Sidekick class %s has no proficiencies\", getName());\n                return null;\n            }\n            return new StartingEquipment(\n                    sidekickProficiencies.savingThrows(),\n                    sidekickProficiencies.skills(),\n                    sidekickProficiencies.weapons(),\n                    sidekickProficiencies.tools(),\n                    sidekickProficiencies.armor(),\n                    \"\",\n                    sources.isClassic());\n        }\n\n        List<String> savingThrows = ClassFields.proficiency.streamFrom(rootNode)\n                .map(n -> SkillOrAbility.format(n.asText(), index(), getSources()))\n                .sorted()\n                .toList();\n\n        JsonNode startingProficiencies = ClassFields.startingProficiencies.getFrom(rootNode);\n\n        var armor = listOfArmorProficiencies(startingProficiencies);\n        var skills = listOfSkillProfiencies(startingProficiencies);\n        var tools = listOfToolProfiencies(startingProficiencies);\n        var weapons = listOfWeaponProfiencies(startingProficiencies);\n\n        return new StartingEquipment(savingThrows, skills, weapons, tools, armor,\n                equipmentDescription(ClassFields.startingEquipment.getFrom(rootNode)),\n                sources.isClassic());\n    }\n\n    Multiclassing buildMulticlassing() {\n        JsonNode multiclassing = ClassFields.multiclassing.getFrom(rootNode);\n        if (multiclassing == null || multiclassing.isEmpty()) {\n            return null;\n        }\n        // primary ablity only exists in 2024 class versions (or homebrew)\n        // requirements only exist in 2014 class versions (or homebrew)\n        String requirements = null;\n        if (ClassFields.requirements.existsIn(multiclassing)) {\n            List<String> reqContents = new ArrayList<>();\n\n            JsonNode reqNode = ClassFields.requirements.getFrom(multiclassing);\n            JsonNode orNode = ClassFields.or.getFrom(reqNode);\n            if (orNode == null) {\n                reqContents.add(\"**Ability Score Minimum:** \" + abilityRequirements(reqNode, \", \"));\n            } else {\n                reqContents.add(\"**Ability Score Minimum:** \" + streamOf(orNode)\n                        .map(n -> abilityRequirements(n, \" or \"))\n                        .collect(Collectors.joining(\"; \")));\n            }\n            appendToText(reqContents, SourceField.entries.getFrom(reqNode), null);\n            requirements = String.join(\"\\n\", reqContents);\n        }\n\n        JsonNode reqSpecialNode = ClassFields.requirementsSpecial.getFrom(multiclassing);\n        String requirementsSpecial = reqSpecialNode == null\n                ? null\n                : replaceText(reqSpecialNode);\n\n        JsonNode profGained = ClassFields.proficienciesGained.getFrom(multiclassing);\n\n        List<String> skillsGained = listOfSkillProfiencies(profGained);\n        List<String> weaponsGained = listOfWeaponProfiencies(profGained);\n        List<String> toolsGained = listOfToolProfiencies(profGained);\n        List<String> armorGained = listOfArmorProficiencies(profGained);\n\n        return new Multiclassing(\n                primaryAbility,\n                requirements,\n                requirementsSpecial,\n                join(\", \", skillsGained),\n                join(\", \", weaponsGained),\n                join(\", \", toolsGained),\n                join(\", \", armorGained),\n                flattenToString(SourceField.entries.getFrom(multiclassing), \"\\n\"),\n                getSources().isClassic());\n    }\n\n    List<String> buildProgressionTable(List<ClassFeature> features, JsonNode sourceNode, ClassFields field) {\n        List<String> headings = new ArrayList<>();\n        List<String> spellCasting = new ArrayList<>();\n        List<String> footnotes = new ArrayList<>();\n\n        Map<Integer, LevelProgression> levels = new HashMap<>();\n        for (int i = 1; i < 21; i++) {\n            // Create LevelProgression for each level\n            // this sets level and proficiency bonus for level\n            levels.put(i, new LevelProgression(i));\n        }\n\n        for (ClassFeature feature : features) {\n            var lp = levels.get(Integer.valueOf(feature.level()));\n            lp.addFeature(feature);\n        }\n\n        for (JsonNode tableNode : field.iterateArrayFrom(sourceNode)) {\n            if (ClassFields.rows.existsIn(tableNode)) {\n                // headings for other/middle columns\n                for (JsonNode label : ClassFields.colLabels.iterateArrayFrom(tableNode)) {\n                    headings.add(stripTableMarkdown(label, footnotes));\n                }\n\n                // values for other/middle columns\n                int i = 1;\n                for (JsonNode row : ClassFields.rows.iterateArrayFrom(tableNode)) {\n                    var lp = levels.get(Integer.valueOf(i));\n                    for (JsonNode col : iterableElements(row)) {\n                        lp.addValue(progressionColumnValue(col));\n                    }\n                    i++;\n                }\n            } else if (ClassFields.rowsSpellProgression.existsIn(tableNode)) {\n                // headings for other/middle columns\n                for (JsonNode label : ClassFields.colLabels.iterateArrayFrom(tableNode)) {\n                    spellCasting.add(replaceText(label));\n                }\n                int i = 1;\n                for (JsonNode row : ClassFields.rowsSpellProgression.iterateArrayFrom(tableNode)) {\n                    var lp = levels.get(Integer.valueOf(i));\n                    for (JsonNode col : iterableElements(row)) {\n                        lp.addSpellSlot(progressionColumnValue(col));\n                    }\n                    i++;\n                }\n            }\n        }\n\n        return progressionAsTable(headings, spellCasting, levels, footnotes);\n    }\n\n    private String stripTableMarkdown(JsonNode label, List<String> footnotes) {\n        String text = markdownLinkToHtml(replaceText(label));\n\n        // Extract footnotes and replace with markers\n        Matcher matcher = footnotePattern.matcher(text);\n\n        return matcher.replaceAll(matchResult -> {\n            String footnoteContent = matchResult.group(1);\n            footnotes.add(footnoteContent);\n            return \" <sup>‡\" + footnotes.size() + \"</sup>\";\n        });\n    }\n\n    List<String> progressionAsTable(List<String> headings,\n            List<String> spellCasting,\n            Map<Integer, LevelProgression> levels,\n            List<String> footnotes) {\n        List<String> text = new ArrayList<>();\n        text.add(\"[!tldr] Class and Feature Progression\");\n        text.add(\"\");\n        text.add(\"<table class=\\\"class-progression\\\">\");\n        text.add(\"<thead>\");\n        // Top-level heading row to group spell casting columns\n        text.add(\"<tr><th colspan='%s'></th>%s</tr>\"\n                .formatted(\n                        3 + headings.size(),\n                        spellCasting.isEmpty()\n                                ? \"\"\n                                : \"<th colspan='%s'>Spell Slots per Spell Level</th>\"\n                                        .formatted(spellCasting.size())));\n\n        text.add(\n                \"<tr class=\\\"class-progression\\\"><th class\\\"level\\\">Level</th><th class\\\"pb\\\">PB</th><th class\\\"feature\\\">Features</th>%s%s</tr>\"\n                        .formatted(\n                                headings.isEmpty()\n                                        ? \"\"\n                                        : \"<th class=\\\"value\\\">\" + join(\"</th><th class=\\\"value\\\">\", headings)\n                                                + \"</th>\",\n                                spellCasting.isEmpty()\n                                        ? \"\"\n                                        : \"<th class=\\\"spellSlot\\\">\"\n                                                + join(\"</th><th class=\\\"spellSlot\\\">\", spellCasting) + \"</th>\"));\n\n        text.add(\"</thead><tbody>\");\n\n        for (int i = 1; i < 21; i++) {\n            var lp = levels.get(Integer.valueOf(i));\n            text.add(\n                    \"<tr class=\\\"class-progression\\\"><td class\\\"level\\\">%s</td><td class\\\"pb\\\">%s</td><td class\\\"feature\\\">%s</td>%s%s</tr>\"\n                            .formatted(\n                                    lp.level, lp.pb,\n                                    join(\", \", lp.features),\n                                    lp.values.isEmpty()\n                                            ? \"\"\n                                            : \"<td class=\\\"value\\\">\" + join(\"</td><td class=\\\"value\\\">\", lp.values)\n                                                    + \"</td>\",\n                                    spellCasting.isEmpty()\n                                            ? \"\"\n                                            : \"<td class=\\\"spellSlot\\\">\"\n                                                    + join(\"</td><td class=\\\"spellSlot\\\">\", lp.spellSlots) + \"</td>\"));\n        }\n\n        text.add(\"</tbody></table>\");\n        if (!footnotes.isEmpty()) {\n            text.add(\"<section class=\\\"footnotes\\\"><ul>\");\n            for (var i = 0; i < footnotes.size(); i++) {\n                text.add(\"<li>‡\" + (i + 1) + \": \" + footnotes.get(i) + \"</li>\");\n            }\n            text.add(\"</ul></section>\");\n        }\n\n        // Move everything into a callout box\n        text.replaceAll(s -> \"> \" + s);\n        text.add(\"\");\n        text.add(\"^class-progression\");\n        return text;\n    }\n\n    String progressionColumnValue(JsonNode c) {\n        if (c == null || c.isNull()) {\n            return \"⏤\";\n        }\n        if (c.isObject()) {\n            String type = ClassFields.type.getTextOrEmpty(c);\n            return switch (type) {\n                case \"dice\" -> {\n                    List<String> rolls = new ArrayList<>();\n                    for (JsonNode roll : ClassFields.toRoll.iterateArrayFrom(c)) {\n                        rolls.add(\"%sd%s\".formatted(\n                                ClassFields.number.getTextOrEmpty(roll),\n                                ClassFields.faces.getTextOrEmpty(roll)));\n                    }\n                    yield join(\",\", rolls);\n                }\n                case \"bonus\", \"bonusSpeed\" -> {\n                    yield \"+\" + ClassFields.value.getTextOrEmpty(c);\n                }\n                default -> throw new IllegalArgumentException(\"Unknown column object value: \" + c.toPrettyString());\n            };\n        }\n        String value = c.asText();\n        return value.isEmpty() || value.equals(\"0\")\n                ? \"⏤\"\n                : replaceText(value);\n    }\n\n    String abilityRequirements(JsonNode reqNode, String joiner) {\n        return streamProps(reqNode)\n                .filter(n -> SkillOrAbility.fromTextValue(n.getKey()) != null)\n                .map(e -> \"%s %s\".formatted(\n                        SkillOrAbility.format(e.getKey(), index(), getSources()),\n                        e.getValue().asText()))\n                .sorted()\n                .collect(Collectors.joining(joiner));\n    }\n\n    List<String> listOfArmorProficiencies(JsonNode containingNode) {\n        return ClassFields.armor.streamFrom(containingNode)\n                .map(n -> {\n                    if (n.isTextual()) {\n                        return armorToLink(n.asText());\n                    }\n                    return armorToLink(ClassFields.full.getTextOrDefault(n,\n                            ClassFields.proficiency.getTextOrEmpty(n)));\n                })\n                .toList();\n    }\n\n    List<String> listOfSkillProfiencies(JsonNode containingNode) {\n        if (isSidekick) {\n            return List.of();\n        }\n\n        return ClassFields.skills.streamFrom(containingNode)\n                // ARRAY of objects\n                .map(n -> {\n                    String choose = null;\n                    List<String> baseSkills = new ArrayList<>();\n                    // any: integer\n                    // choose: {\n                    // count: integer,\n                    // from: [\n                    // ...\n                    // ]\n                    // }\n                    // skillName: true\n                    for (var x : iterableFields(n)) {\n                        if (\"any\".equals(x.getKey())) {\n                            choose = skillChoices(List.of(),\n                                    x.getValue().asInt());\n                        } else if (\"choose\".equals(x.getKey())) {\n                            choose = skillChoices(\n                                    ClassFields.from.getListOfStrings(x.getValue(), tui()),\n                                    ClassFields.count.intOrDefault(x.getValue(), 1));\n                        } else {\n                            SkillOrAbility skill = index.findSkillOrAbility(n.asText(), getSources());\n                            if (skill != null) {\n                                baseSkills.add(linkifySkill(skill));\n                            }\n                        }\n                    }\n\n                    String allSkills = joinConjunct(\" and \", baseSkills);\n                    if (baseSkills.size() > 0 && choose != null) {\n                        return \"%s; and %s\".formatted(allSkills, choose);\n                    }\n                    return choose == null ? allSkills : choose;\n                })\n                .toList();\n    }\n\n    List<String> listOfWeaponProfiencies(JsonNode containingNode) {\n        return ClassFields.weapons.streamFrom(containingNode)\n                .map(n -> {\n                    if (ClassFields.optional.booleanOrDefault(n, false)) {\n                        return \"%s (optional)\".formatted(replaceText(ClassFields.proficiency.getTextOrEmpty(n)));\n                    }\n                    String weaponType = n.asText();\n                    if (weaponType.matches(\"(?i)(simple|martial)\")) {\n                        return \"%s weapons\".formatted(\n                                sources.isClassic() ? weaponType : toTitleCase(weaponType));\n                    }\n                    return replaceText(weaponType);\n                })\n                .toList();\n    }\n\n    List<String> listOfToolProfiencies(JsonNode containingNode) {\n        return ClassFields.tools.streamFrom(containingNode)\n                .map(this::replaceText)\n                .toList();\n    }\n\n    String armorToLink(String armor) {\n        return armor\n                .replaceAll(\"^light\", linkify(Tools5eIndexType.itemType,\n                        sources.isClassic() ? \"la|phb|light armor\" : \"la|xphb|Light armor\"))\n                .replaceAll(\"^medium\", linkify(Tools5eIndexType.itemType,\n                        sources.isClassic() ? \"ma|phb|medium armor\" : \"ma|xphb|Medium armor\"))\n                .replaceAll(\"^heavy\", linkify(Tools5eIndexType.itemType,\n                        sources.isClassic() ? \"ha|phb|heavy armor\" : \"ha|xphb|Heavy armor\"))\n                .replaceAll(\"^shields?\", linkify(Tools5eIndexType.item,\n                        sources.isClassic() ? \"shield|phb|shields\" : \"shield|xphb|Shields\"));\n    }\n\n    String skillChoices(Collection<String> skills, int numSkills) {\n        if (skills.isEmpty() || skills.size() >= 18) {\n            String link = \"||skill%s\".formatted(numSkills == 1 ? \"\" : \"s\");\n            String linkToSkills = linkifyRules(Tools5eIndexType.skill, link);\n            return sources.isClassic()\n                    ? \"choose any %s %s\".formatted(numSkills, linkToSkills)\n                    : \"Choose %s %s\".formatted(numSkills, linkToSkills);\n        }\n\n        List<String> formatted = skills.stream().map(x -> index.findSkillOrAbility(x, getSources()))\n                .filter(x -> x != null)\n                .sorted(SkillOrAbility.comparator)\n                .map(x -> linkifySkill(x))\n                .toList();\n        return sources.isClassic()\n                ? \"choose %s from %s\".formatted(numSkills,\n                        joinConjunct(\" and \", formatted))\n                : \"*Choose %s:* %s\".formatted(numSkills,\n                        joinConjunct(\" or \", formatted));\n    }\n\n    String equipmentDescription(JsonNode startingEquipment) {\n        List<String> text = new ArrayList<>();\n\n        if (ClassFields.additionalFromBackground.existsIn(startingEquipment)\n                && ClassFields.defaultEquipment.existsIn(startingEquipment)) {\n            // Older default format.\n            if (ClassFields.additionalFromBackground.booleanOrDefault(startingEquipment, false)) {\n                text.add(\"You start with the following items, plus anything provided by your background.\");\n                text.add(\"\");\n            }\n\n            for (JsonNode item : ClassFields.defaultEquipment.iterateArrayFrom(startingEquipment)) {\n                text.add(\"- %s\".formatted(replaceText(item)));\n            }\n\n            String goldAlternative = ClassFields.goldAlternative.getTextOrNull(startingEquipment);\n            if (isPresent(goldAlternative)) {\n                text.add(\"\");\n                text.add(\"Alternatively, you may start with %s gp to buy your own equipment.\"\n                        .formatted(replaceText(goldAlternative)));\n            }\n        } else {\n            JsonNode entries = SourceField.entries.getFrom(startingEquipment);\n            appendToText(text, entries, null);\n        }\n        return String.join(\"\\n\", text);\n    }\n\n    static ClassFeature findClassFeature(JsonSource converter, Tools5eIndexType type, JsonNode cf, String fieldName) {\n        String lookup = cf.isTextual() ? cf.asText() : cf.get(fieldName).asText();\n\n        String finalKey = type.fromTagReference(lookup);\n        ClassFeature feature = keyToClassFeature.get(finalKey);\n        if (feature == null) {\n            JsonNode cfNode = converter.index().getNode(finalKey);\n            if (cfNode == null) {\n                return null; // skipped or not found\n            }\n            feature = new ClassFeature(type, finalKey, cfNode);\n            keyToClassFeature.putIfAbsent(finalKey, feature);\n        }\n        return feature;\n    }\n\n    static ClassFeature getClassFeature(String featureKey) {\n        return keyToClassFeature.get(featureKey);\n    }\n\n    static record ClassFeature(\n            Tools5eIndexType cfType,\n            JsonNode cfNode,\n            Tools5eSources cfSources,\n            KeyData keyData) {\n        public ClassFeature(Tools5eIndexType cfType, String key, JsonNode cfNode) {\n            this(cfType, cfNode, Tools5eSources.findSources(key),\n                    cfType == Tools5eIndexType.classfeature\n                            ? new ClassFeatureKeyData(key)\n                            : new SubclassFeatureKeyData(key));\n        }\n\n        protected Tools5eLinkifier linkifier() {\n            return Tools5eLinkifier.instance();\n        }\n\n        public String getName() {\n            return cfSources.getName();\n        }\n\n        public String level() {\n            return keyData.level();\n        }\n\n        void appendLink(JsonSource converter, List<String> text, String pageSource) {\n            converter.maybeAddBlankLine(text);\n            String x = linkifier().decoratedFeatureTypeName(cfSources, cfNode);\n            text.add(String.format(\"[%s](#%s)\", x, toAnchorTag(x + \" (Level \" + level() + \")\")));\n        }\n\n        public void appendListItemText(JsonSource converter, List<String> text, String pageSource) {\n            boolean pushed = converter.parseState().pushFeatureType();\n            try {\n                text.add(\"**\" + linkifier().decoratedFeatureTypeName(cfSources, cfNode) + \"**\");\n                if (!cfSources.primarySource().equalsIgnoreCase(pageSource)) {\n                    text.add(converter.getLabeledSource(cfSources));\n                }\n                text.add(\"\");\n                converter.appendToText(text, SourceField.entries.getFrom(cfNode), null);\n                text.add(\"\");\n            } finally {\n                converter.parseState().pop(pushed);\n            }\n        }\n\n        void appendIntroText(JsonSource converter, List<String> text, String primarySource) {\n            boolean pushed = converter.parseState().pushFeatureType();\n            boolean hasHeading = false;\n            boolean isSubclassFeature = false;\n            boolean isEntries = false;\n            boolean isSpellsTable = false;\n            AppendTypeValue type = null;\n            String name = \"\";\n            String caption = \"\";\n\n            try {\n                for (JsonNode entry : converter.iterableElements(SourceField.entries.getFrom(cfNode))) {\n                    type = AppendTypeValue.valueFrom(entry, SourceField.type);\n                    name = SourceField.name.replaceTextFrom(entry, converter);\n                    caption = TableFields.caption.getTextOrEmpty(entry);\n\n                    // Any of the following indicate the end of the flavor text, so inject the heading here\n                    // refSubclassFeature, named entries, Cleric Domain Spells Table\n                    // \"At each indicated cleric level, you add the listed spells to your spells prepared.\"\n                    isSubclassFeature = !hasHeading && AppendTypeValue.refSubclassFeature.equals(type);\n                    isEntries = !hasHeading && !name.isEmpty() && AppendTypeValue.entries.equals(type);\n                    isSpellsTable = !hasHeading && (entry.asText().startsWith(\"At each indicated \") ||\n                            (AppendTypeValue.table.equals(type) && caption.contains(\"Spell\")));\n\n                    if (isSubclassFeature || isEntries || isSpellsTable) {\n                        converter.maybeAddBlankLine(text);\n                        text.add(\"## Subclass Features\");\n                        if (isEntries) {\n                            converter.maybeAddBlankLine(text);\n                            text.add(\"### \" + name + \" (Level \" + level() + \")\");\n                            entry = SourceField.entries.getFrom(entry);\n                        } else if (isSpellsTable) {\n                            converter.maybeAddBlankLine(text);\n                            text.add(\"### Domain Spells (Level \" + level() + \")\");\n                        }\n                        hasHeading = true;\n                    }\n                    converter.maybeAddBlankLine(text);\n                    converter.appendToText(text, entry, null);\n                }\n            } finally {\n                converter.parseState().pop(pushed);\n            }\n        }\n\n        void appendText(JsonSource converter, List<String> text, String primarySource) {\n            boolean pushed = converter.parseState().pushFeatureType();\n            try {\n                converter.maybeAddBlankLine(text);\n                text.add(\"### \" + linkifier().decoratedFeatureTypeName(cfSources, cfNode) + \" (Level \" + level() + \")\");\n                if (!cfSources.primarySource().equalsIgnoreCase(primarySource)) {\n                    text.add(converter.getLabeledSource(cfSources));\n                }\n                converter.maybeAddBlankLine(text);\n                converter.appendToText(text, SourceField.entries.getFrom(cfNode), null);\n            } finally {\n                converter.parseState().pop(pushed);\n            }\n        }\n    }\n\n    static interface KeyData {\n        String name();\n\n        String parentName();\n\n        String parentSource();\n\n        String level();\n\n        String itemSource();\n    }\n\n    static class ClassFeatureKeyData implements KeyData {\n        final String cfName;\n        final String className;\n        final String classSource;\n        final String level;\n        final String cfSource;\n\n        public ClassFeatureKeyData(String key) {\n            String[] parts = key.split(\"\\\\|\");\n            this.cfName = parts[1];\n            this.className = parts[2];\n            this.classSource = parts[3];\n            this.level = parts[4];\n            this.cfSource = parts[5];\n        }\n\n        @Override\n        public String name() {\n            return cfName;\n        }\n\n        @Override\n        public String parentName() {\n            return className;\n        }\n\n        @Override\n        public String parentSource() {\n            return classSource;\n        }\n\n        @Override\n        public String level() {\n            return level;\n        }\n\n        @Override\n        public String itemSource() {\n            return cfSource;\n        }\n    }\n\n    // Unpack a subclass key\n    public static class SubclassKeyData implements KeyData {\n        String scName;\n        String className;\n        String classSource;\n        String scSource;\n\n        public SubclassKeyData(String key) {\n            String[] parts = key.split(\"\\\\|\");\n            this.scName = parts[1];\n            this.className = parts[2];\n            this.classSource = parts[3];\n            this.scSource = parts[4];\n        }\n\n        @Override\n        public String name() {\n            return scName;\n        }\n\n        @Override\n        public String parentName() {\n            return className;\n        }\n\n        @Override\n        public String parentSource() {\n            return classSource;\n        }\n\n        @Override\n        public String level() {\n            return \"\";\n        }\n\n        @Override\n        public String itemSource() {\n            return scSource;\n        }\n    }\n\n    // Unpack a subclass feature key\n    static class SubclassFeatureKeyData implements KeyData {\n        String scfName;\n        String className;\n        String classSource;\n        String scName;\n        String scSource;\n        String level;\n        String scfSource;\n\n        public SubclassFeatureKeyData(String key) {\n            String[] parts = key.split(\"\\\\|\");\n            this.scfName = parts[1];\n            this.className = parts[2];\n            this.classSource = parts[3];\n            this.scName = parts[4];\n            this.scSource = parts[5];\n            this.level = parts[6];\n            this.scfSource = parts[7];\n        }\n\n        @Override\n        public String name() {\n            return scfName;\n        }\n\n        @Override\n        public String parentName() {\n            return scName;\n        }\n\n        @Override\n        public String parentSource() {\n            return scSource;\n        }\n\n        @Override\n        public String level() {\n            return level;\n        }\n\n        @Override\n        public String itemSource() {\n            return scfSource;\n        }\n\n        public String toKey() {\n            return String.join(\"|\",\n                    Tools5eIndexType.subclassFeature.name(),\n                    scfName,\n                    className, classSource,\n                    scName, scSource,\n                    level, scfSource)\n                    .toLowerCase();\n        }\n\n        public String toSubclassKey() {\n            return String.join(\"|\",\n                    Tools5eIndexType.subclass.name(),\n                    scName,\n                    className, classSource,\n                    scSource)\n                    .toLowerCase();\n        }\n\n        public String toClassKey() {\n            return String.join(\"|\",\n                    Tools5eIndexType.classtype.name(),\n                    className, classSource)\n                    .toLowerCase();\n        }\n    }\n\n    static class LevelProgression {\n        final String level;\n        final String pb;\n        List<String> features = new ArrayList<>();\n        List<String> values = new ArrayList<>();\n        List<String> spellSlots = new ArrayList<>();\n\n        LevelProgression(int level) {\n            this.level = toOrdinal(level);\n            this.pb = \"+\" + JsonSource.levelToPb(level);\n        }\n\n        void addFeature(ClassFeature cf) {\n            features.add(toHtmlLink(cf.getName(), cf.level()));\n        }\n\n        void addValue(String value) {\n            values.add(value);\n        }\n\n        void addSpellSlot(String value) {\n            spellSlots.add(value);\n        }\n\n        String toHtmlLink(String x, String level) {\n            return String.format(\"<a href='#%s' class='internal-link'>%s</a>\",\n                    x + \" (Level \" + level + \")\", x);\n        }\n    }\n\n    // Not static. Relies on Json2QuteClass members\n    class SidekickProficiencies {\n        static final Pattern sidekickArmor = Pattern.compile(\"(?<=with ).*? armor\");\n        static final Pattern sidekickHumanoid = Pattern.compile(\"[Ii]f it is a humanoid[^.]+\\\\.($| )\");\n        static final Pattern sidekickSavingThrows = Pattern.compile(\"\\\\b[^ ]+ saving throw of your choice.*\");\n        static final Pattern sidekickSkills = Pattern\n                .compile(\"\\\\b[^ ]+ skills of your choice(,| from the following list.*)\");\n        static final Pattern sidekickTools = Pattern.compile(\"\\\\b[^ ]+ tools of your choice\");\n        static final Pattern sidekickWeapons = Pattern.compile(\"(?<=(with|and) )all simple( and martial)? weapons\");\n\n        private String armor;\n        private String skills;\n        private String savingThrows;\n        private String tools;\n        private String weapons;\n\n        SidekickProficiencies(JsonNode node) {\n            String text = String.join(\"\\n\", SourceField.entries.replaceTextFromList(node, index()));\n            String humanoidClause = null;\n\n            Matcher humanoidMatcher = sidekickHumanoid.matcher(text);\n            if (humanoidMatcher.find()) {\n                humanoidClause = humanoidMatcher.group(0);\n                // Remove the humanoid clause from the text\n                text = humanoidMatcher.replaceAll(\"\");\n            }\n\n            Matcher armorMatcher = sidekickArmor.matcher(text);\n            if (armorMatcher.find()) {\n                armor = uppercaseFirst(armorMatcher.group());\n                if (humanoidClause.contains(\"shields\")) {\n                    armor += \"; and shields if [humanoid](#%s)\".formatted(toAnchorTag(\"Bonus Proficiencies (Level 1)\"));\n                }\n            }\n\n            Matcher savingThrowsMatcher = sidekickSavingThrows.matcher(text);\n            if (savingThrowsMatcher.find()) {\n                savingThrows = uppercaseFirst(savingThrowsMatcher.group());\n            }\n\n            Matcher skillsMatcher = sidekickSkills.matcher(text);\n            if (skillsMatcher.find()) {\n                skills = uppercaseFirst(skillsMatcher.group()).replaceAll(\",$\", \"\");\n            }\n\n            // Only present in the humanoid clause\n            Matcher toolMatcher = sidekickTools.matcher(humanoidClause);\n            if (toolMatcher.find()) {\n                tools = \"%s if [humanoid](#%s)\".formatted(\n                        uppercaseFirst(toolMatcher.group()),\n                        toAnchorTag(\"Bonus Proficiencies (Level 1)\"));\n            }\n\n            // Only present in the humanoid clause\n            Matcher weaponsMatcher = sidekickWeapons.matcher(humanoidClause);\n            if (weaponsMatcher.find()) {\n                weapons = \"%s if [humanoid](#%s)\".formatted(\n                        uppercaseFirst(weaponsMatcher.group()),\n                        toAnchorTag(\"Bonus Proficiencies (Level 1)\"));\n            }\n        }\n\n        List<String> armor() {\n            return isPresent(armor) ? List.of(armor) : List.of();\n        }\n\n        List<String> savingThrows() {\n            return isPresent(savingThrows) ? List.of(savingThrows) : List.of();\n        }\n\n        List<String> skills() {\n            return isPresent(skills) ? List.of(skills) : List.of();\n        }\n\n        List<String> tools() {\n            return isPresent(tools) ? List.of(tools) : List.of();\n        }\n\n        List<String> weapons() {\n            return isPresent(weapons) ? List.of(weapons) : List.of();\n        }\n    }\n\n    enum ClassFields implements JsonNodeReader {\n        additionalFromBackground,\n        additionalProperties,\n        any,\n        armor,\n        choose,\n        classFeature,\n        classFeatures,\n        classTableGroups,\n        colLabels,\n        count,\n        defaultEquipment(\"default\"), // default is a reserved word\n        faces,\n        from,\n        full,\n        gainSubclassFeature,\n        goldAlternative,\n        hd,\n        isSidekick,\n        multiclassing,\n        number,\n        optional,\n        optionalfeatureProgression,\n        or,\n        primaryAbility,\n        proficienciesGained,\n        proficiency,\n        properties,\n        required,\n        requirements,\n        requirementsSpecial,\n        rows,\n        rowsSpellProgression,\n        shortName,\n        skills,\n        startingEquipment,\n        startingProficiencies,\n        subclassFeature,\n        subclassFeatures,\n        subclassShortName,\n        subclassSource,\n        subclassTableGroups,\n        subclassTitle,\n        toRoll,\n        tools,\n        type,\n        value,\n        weapons,\n        ;\n\n        final String nodeName;\n\n        ClassFields() {\n            nodeName = name();\n        }\n\n        ClassFields(String nodeName) {\n            this.nodeName = nodeName;\n        }\n\n        @Override\n        public String nodeName() {\n            return nodeName;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCommon.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static dev.ebullient.convert.StringUtil.isPresent;\nimport static dev.ebullient.convert.StringUtil.joinConjunct;\nimport static dev.ebullient.convert.StringUtil.toOrdinal;\n\nimport java.nio.file.Path;\nimport java.text.Normalizer;\nimport java.text.Normalizer.Form;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.Optional;\nimport java.util.regex.Pattern;\nimport java.util.stream.StreamSupport;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.ArrayNode;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\n\nimport dev.ebullient.convert.StringUtil;\nimport dev.ebullient.convert.io.Msg;\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.qute.NamedText;\nimport dev.ebullient.convert.tools.JsonNodeReader;\nimport dev.ebullient.convert.tools.ToolsIndex.TtrpgValue;\nimport dev.ebullient.convert.tools.dnd5e.Json2QuteMonster.MonsterFields;\nimport dev.ebullient.convert.tools.dnd5e.qute.AbilityScores;\nimport dev.ebullient.convert.tools.dnd5e.qute.AcHp;\nimport dev.ebullient.convert.tools.dnd5e.qute.ImmuneResist;\nimport dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase;\nimport dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteNote;\n\npublic class Json2QuteCommon implements JsonSource {\n    static final Pattern featPattern = Pattern.compile(\"([^|]+)\\\\|?.*\");\n    static final List<String> SPEED_MODE = List.of(\"walk\", \"burrow\", \"climb\", \"fly\", \"swim\");\n    static final List<String> specialTraits = List.of(\"special equipment\", \"shapechanger\");\n    static final Map<String, String> SCF_TYPE_TO_NAME = Map.of(\n            \"arcane\", \"Arcane Focus\",\n            \"druid\", \"Druidic Focus\",\n            \"holy\", \"Holy Symbol\");\n\n    static final Comparator<Entry<String, List<String>>> compareNumberStrings = Comparator\n            .comparingInt(e -> Integer.parseInt(e.getKey()));\n\n    protected final Tools5eIndex index;\n    protected final Tools5eSources sources;\n    protected final Tools5eIndexType type;\n    protected final JsonNode rootNode;\n    protected String imagePath = null;\n\n    Json2QuteCommon(Tools5eIndex index, Tools5eIndexType type, JsonNode jsonNode) {\n        this.index = index;\n        this.rootNode = jsonNode;\n        this.type = type;\n        this.sources = type.multiNode() ? null : Tools5eSources.findOrTemporary(jsonNode);\n    }\n\n    public Json2QuteCommon withImagePath(String imagePath) {\n        this.imagePath = imagePath;\n        return this;\n    }\n\n    public String getName() {\n        return this.sources.getName();\n    }\n\n    public Tools5eLinkifier linkifier() {\n        return Tools5eLinkifier.instance();\n    }\n\n    @Override\n    public Tools5eSources getSources() {\n        return sources;\n    }\n\n    @Override\n    public Tools5eIndex index() {\n        return index;\n    }\n\n    @Override\n    public String getImagePath() {\n        if (imagePath != null) {\n            return imagePath;\n        }\n        return JsonSource.super.getImagePath();\n    }\n\n    public String getText(String heading) {\n        List<String> text = new ArrayList<>();\n        appendToText(text, SourceField.entries.getFrom(rootNode), heading);\n        return text.isEmpty() ? null : String.join(\"\\n\", text);\n    }\n\n    public String getFluffDescription(Tools5eIndexType fluffType, String heading, List<ImageRef> images) {\n        return getFluffDescription(rootNode, fluffType, heading, images);\n    }\n\n    public String getFluffDescription(JsonNode fromNode, Tools5eIndexType fluffType, String heading, List<ImageRef> images) {\n        List<String> text = getFluff(fromNode, fluffType, heading, images);\n        return text.isEmpty() ? null : String.join(\"\\n\", text);\n    }\n\n    public List<String> getFluff(Tools5eIndexType fluffType, String heading, List<ImageRef> images) {\n        return getFluff(rootNode, fluffType, heading, images);\n    }\n\n    public List<String> getFluff(JsonNode fromNode, Tools5eIndexType fluffType, String heading, List<ImageRef> images) {\n        List<String> text = new ArrayList<>();\n        JsonNode fluffNode = null;\n        if (TtrpgValue.indexFluffKey.existsIn(fromNode)) {\n            // Specific variant\n            String fluffKey = TtrpgValue.indexFluffKey.getTextOrEmpty(fromNode);\n            fluffNode = index.getOrigin(fluffKey);\n        } else if (Tools5eFields.fluff.existsIn(fromNode)) {\n            fluffNode = Tools5eFields.fluff.getFrom(fromNode);\n            JsonNode monsterFluff = Tools5eFields._monsterFluff.getFrom(fluffNode);\n            if (monsterFluff != null) {\n                String fluffKey = fluffType.createKey(monsterFluff);\n                fluffNode = index.getOrigin(fluffKey);\n            }\n        } else if (Tools5eFields.hasFluff.booleanOrDefault(fromNode, false)\n                || Tools5eFields.hasFluffImages.booleanOrDefault(fromNode, false)) {\n            String fluffKey = fluffType.createKey(fromNode);\n            fluffNode = index.getOrigin(fluffKey);\n        }\n\n        if (fluffNode != null) {\n            unpackFluffNode(fluffType, fluffNode, text, heading, images);\n        }\n        return text;\n    }\n\n    public List<ImageRef> getFluffImages(Tools5eIndexType fluffType) {\n        List<ImageRef> images = new ArrayList<>();\n        if (Tools5eFields.hasFluffImages.booleanOrDefault(rootNode, false)) {\n            String fluffKey = fluffType.createKey(rootNode);\n            JsonNode fluffNode = index.getOrigin(fluffKey);\n            if (fluffNode != null) {\n                getImages(Tools5eFields.images.getFrom(fluffNode), images);\n            }\n        }\n        return images;\n    }\n\n    // {\"ability\":[{\"dex\":13}]}\n    private String abilityPrereq(JsonNode abilityPrereq) {\n        ArrayNode elements = ensureArray(abilityPrereq);\n\n        boolean multipleInner = false;\n        boolean multiMultipleInner = false;\n        JsonNode allValuesEqual = null;\n\n        // See if all of the abilities have the same value\n        outer: for (JsonNode abMetaNode : elements) {\n            ObjectNode objectNode = (ObjectNode) abMetaNode;\n\n            for (JsonNode valueNode : objectNode) {\n                if (allValuesEqual == null) {\n                    allValuesEqual = valueNode;\n                } else {\n                    var ave = allValuesEqual;\n                    boolean allMatch = StreamSupport.stream(objectNode.spliterator(), false)\n                            .allMatch(node -> node.equals(ave));\n                    if (!allMatch) {\n                        allValuesEqual = null;\n                        break outer;\n                    }\n                }\n            }\n        }\n\n        List<String> abilityOptions = new ArrayList<>();\n        for (JsonNode abMetaNode : elements) {\n            if (allValuesEqual != null) {\n                List<String> options = new ArrayList<>();\n                multipleInner |= abMetaNode.size() > 1;\n                abMetaNode.fieldNames().forEachRemaining(x -> {\n                    options.add(SkillOrAbility.format(x, index(), getSources()));\n                });\n                abilityOptions.add(joinConjunct(\" and \", options));\n            } else {\n                Map<String, List<String>> groups = new HashMap<>();\n                for (Entry<String, JsonNode> score : iterableFields(abMetaNode)) {\n                    groups.computeIfAbsent(score.getValue().asText(), k -> new ArrayList<>())\n                            .add(SkillOrAbility.format(score.getKey(), index(), sources));\n                }\n\n                boolean isMulti = groups.values().stream().anyMatch(x -> x.size() > 1);\n                multiMultipleInner |= isMulti;\n                multipleInner |= isMulti;\n\n                List<String> byScore = groups.entrySet().stream()\n                        .sorted((a, b) -> compareNumberStrings.compare(b, a))\n                        .map(e -> {\n                            List<String> abs = e.getValue().stream()\n                                    .map(x -> index().findSkillOrAbility(x, sources))\n                                    .sorted(SkillOrAbility.comparator)\n                                    .map(x -> x.value())\n                                    .toList();\n                            return String.format(\"%s %s or higher\",\n                                    joinConjunct(\" and \", abs),\n                                    e.getKey());\n                        })\n                        .toList();\n\n                abilityOptions.add(isMulti\n                        ? joinConjunct(\"; \", \" and \", byScore)\n                        : joinConjunct(\" and \", byScore));\n            }\n        }\n\n        var isComplex = multipleInner || multiMultipleInner || allValuesEqual == null;\n        String joined = joinConjunct(\n                multiMultipleInner ? \" - \" : multipleInner ? \"; \" : \", \",\n                isComplex ? \" OR \" : \" or \",\n                abilityOptions);\n\n        return joined + (allValuesEqual != null\n                ? \" \" + allValuesEqual.asText() + \" or higher\"\n                : \"\");\n    }\n\n    // {\"name\":\"Rune Carver\",\"displayEntry\":\"{@background Rune Carver|BGG}\"}]\n    private String backgroundPrereq(JsonNode backgroundPrereq) {\n        List<String> backgrounds = new ArrayList<>();\n        for (JsonNode p : iterableElements(backgroundPrereq)) {\n            JsonNode displayEntry = PrereqFields.displayEntry.getFrom(p);\n            if (displayEntry != null) {\n                backgrounds.add(replaceText(displayEntry.asText()));\n            } else {\n                String name = SourceField.name.getTextOrEmpty(p);\n                backgrounds.add(index.linkifyByName(Tools5eIndexType.background, name));\n            }\n        }\n        return joinConjunct(\" or \", backgrounds);\n    }\n\n    private String replaceConjoinOr(JsonNode campaignPrereq, String suffix) {\n        List<String> cmpn = new ArrayList<>();\n        for (JsonNode p : iterableElements(campaignPrereq)) {\n            cmpn.add(replaceText(p.asText()));\n        }\n        return joinConjunct(\" or \", cmpn) + suffix;\n    }\n\n    private String expertisePrereq(JsonNode expertisePrereq) {\n        // \"prerequisite\": [\n        //     {\n        //         \"expertise\": [\n        //             {\n        //                 \"skill\": true\n        //             }\n        //         ]\n        //     }\n        // ],\n        List<String> expertise = new ArrayList<>();\n        for (JsonNode p : iterableElements(expertisePrereq)) {\n            for (Entry<String, JsonNode> prof : iterableFields(p)) {\n                switch (prof.getKey()) {\n                    case \"skill\" -> {\n                        if (prof.getValue().asBoolean()) {\n                            expertise.add(\"Expertise in a skill\");\n                        } else {\n                            tui().warnf(Msg.UNKNOWN, \"unknown expertise prereq value %s from %s / %s\",\n                                    p.toString(), getSources().getKey(), parseState().getSource());\n                        }\n                    }\n                    default -> {\n                        tui().warnf(Msg.UNKNOWN, \"unknown expertise prereq type %s from %s / %s\",\n                                p.toString(), getSources().getKey(), parseState().getSource());\n                    }\n                }\n            }\n        }\n        return joinConjunct(\" or \", expertise);\n    }\n\n    // \"scion of the outer planes|ua2022wondersofthemultiverse|scion of the outer planes (good outer plane)\"\n    // \"scion of the outer planes|sato|scion of the outer planes (good outer plane)\"\n    private String featPrereq(JsonNode featPrereq) {\n        List<String> feats = new ArrayList<>();\n        for (JsonNode p : iterableElements(featPrereq)) {\n            replaceText(String.format(\"{@feat %s} feat\", p.asText()));\n        }\n        return joinConjunct(\" or \", feats);\n    }\n\n    private String itemTypePrereq(JsonNode itemTypePrereq) {\n        List<String> types = new ArrayList<>();\n        for (JsonNode p : iterableElements(itemTypePrereq)) {\n            ItemType type = index.findItemType(p.asText(), getSources());\n            if (type != null) {\n                types.add(type.linkify());\n            } else {\n                tui().warnf(Msg.UNKNOWN, \"unknown item type prereq %s from %s / %s\",\n                        p.asText(), getSources().getKey(), parseState().getSource());\n            }\n        }\n        return joinConjunct(\" and \", types);\n    }\n\n    private String itemPropertyPrereq(JsonNode itemPropertyPrereq) {\n        List<String> props = new ArrayList<>();\n        for (JsonNode p : iterableElements(itemPropertyPrereq)) {\n            ItemProperty prop = index.findItemProperty(p.asText(), getSources());\n            if (prop != null) {\n                props.add(prop.linkify());\n            }\n        }\n        return joinConjunct(\" and \", props);\n    }\n\n    // \"level\":4\n    // \"level\":{\"level\":1,\"class\":{\"name\":\"Fighter\",\"visible\":true}}}\n    private String levelPrereq(JsonNode levelPrereq) {\n        if (levelPrereq.isArray())\n            tui().errorf(\"levelPrereq: Array parameter\");\n\n        if (levelPrereq.isNumber()) {\n            return toOrdinal(levelPrereq.asInt());\n        }\n\n        String level = Tools5eFields.level.getTextOrThrow(levelPrereq);\n        JsonNode classNode = SourceField._class_.getFrom(levelPrereq);\n        JsonNode subclassNode = Tools5eFields.subclass.getFrom(levelPrereq);\n\n        // neither class nor subclass is defined\n        if (classNode == null && subclassNode == null) {\n            return toOrdinal(level);\n        }\n\n        boolean isLevelVisible = !\"1\".equals(level); // hide implied first level\n        boolean isSubclassVisible = Tools5eFields.visible.booleanOrDefault(subclassNode, false);\n        boolean isClassVisible = classNode != null\n                && (isSubclassVisible || Tools5eFields.visible.booleanOrDefault(classNode, false));\n\n        String classPart = \"\";\n        if (isClassVisible && isSubclassVisible) {\n            classPart = String.format(\"%s (%s)\",\n                    SourceField.name.getTextOrEmpty(classNode),\n                    SourceField.name.getTextOrEmpty(subclassNode));\n        } else if (isClassVisible) {\n            classPart = SourceField.name.getTextOrEmpty(classNode);\n        } else if (isSubclassVisible) {\n            tui().warnf(\"Subclass %s without class in %s\", subclassNode, levelPrereq);\n        }\n\n        String levelPart = isLevelVisible\n                ? String.format(\"Level %s\", level)\n                : \"\";\n\n        return levelPart\n                + (isClassVisible ? \" \" + classPart : \"\");\n    }\n\n    // {\"proficiency\":[{\"armor\":\"medium\"}]}\n    // {\"proficiency\":[{\"weaponGroup\":\"martial\"}]}\n    private String proficiencyPrereq(JsonNode profPrereq) {\n        List<String> profs = new ArrayList<>();\n        for (JsonNode p : iterableElements(profPrereq)) {\n            for (Entry<String, JsonNode> prof : iterableFields(p)) {\n                switch (prof.getKey()) {\n                    case \"armor\" -> profs.add(String.format(\"%s armor\",\n                            replaceText(prof.getValue().asText())));\n                    case \"weapon\" -> profs.add(String.format(\"a %s weapon\",\n                            replaceText(prof.getValue().asText())));\n                    case \"weaponGroup\" -> profs.add(String.format(\"%s weapons\",\n                            replaceText(prof.getValue().asText())));\n                    default -> {\n                        tui().warnf(Msg.UNKNOWN, \"unknown proficiency prereq %s from %s / %s\",\n                                p.toString(), getSources().getKey(), parseState().getSource());\n                    }\n                }\n            }\n        }\n        return String.format(\"Proficiency with %s\", joinConjunct(\" or \", profs));\n    }\n\n    // [{\"name\":\"elf\"}]\n    // [{\"name\":\"half-elf\"}]\n    // [{\"name\":\"small race\",\"displayEntry\":\"a Small race\"}]\n    private String racePrereq(JsonNode racePrereq) {\n        List<String> races = new ArrayList<>();\n        for (JsonNode p : iterableElements(racePrereq)) {\n            JsonNode displayEntry = PrereqFields.displayEntry.getFrom(p);\n            if (displayEntry != null) {\n                races.add(replaceText(displayEntry.asText()));\n            } else {\n                String name = SourceField.name.getTextOrEmpty(p);\n                String subraceName = Tools5eFields.subrace.getTextOrNull(p);\n                races.add(index.linkifyByName(Tools5eIndexType.race, Json2QuteRace.getSubraceName(name, subraceName)));\n            }\n        }\n        return joinConjunct(\" or \", races);\n    }\n\n    private String scfPrereq(JsonNode scfPrereq) {\n        if (scfPrereq.isBoolean()) {\n            return replaceText(\"Ability to use a {@variantrule Spellcasting Focus|XPHB}\");\n        }\n\n        List<String> scfTypes = new ArrayList<>();\n        for (JsonNode p : iterableElements(scfPrereq)) {\n            String type = p.asText();\n            String name = SCF_TYPE_TO_NAME.getOrDefault(type, type);\n            String article = scfTypes.isEmpty()\n                    ? articleFor(name) + \" \"\n                    : \"\";\n\n            if (!name.equals(type)) {\n                name = replaceText(\"{@item \" + name + \"|XPHB}\");\n            }\n\n            scfTypes.add(\"%s%s\".formatted(article, name));\n        }\n\n        return replaceText(\"Ability to use %s as a {@variantrule Spellcasting Focus|XPHB}\"\n                .formatted(joinConjunct(\" or \", scfTypes)));\n    }\n\n    private List<String> testBoolean(JsonNode node, String valueIfTrue) {\n        return node.booleanValue()\n                ? List.of(valueIfTrue)\n                : List.of();\n    }\n\n    private String spellPrereq(JsonNode spellPrereq) {\n        List<String> spells = new ArrayList<>();\n        for (JsonNode p : iterableElements(spellPrereq)) {\n            if (p.isTextual()) {\n                String[] split = p.asText().split(\"#\");\n                if (split.length == 1) {\n                    spells.add(replaceText(String.format(\"{@spell %s}\", split[0])));\n                } else if (\"c\".equals(split[1])) {\n                    spells.add(replaceText(String.format(\"{@spell %s} cantrip\", split[0])));\n                } else if (\"x\".equals(split[1])) {\n                    spells.add(replaceText(String.format(\"{@spell hex} spell or a warlock feature that curses\", split[0])));\n                } else {\n                    tui().warnf(Msg.UNKNOWN, \"unknown spell prereq %s from %s / %s\",\n                            p.toString(), getSources().getKey(), parseState().getSource());\n                }\n            } else {\n                spells.add(replaceText(String.format(\"{@filter %s|spells|%s}\",\n                        SourceField.entry.getTextOrEmpty(p),\n                        PrereqFields.choose.getTextOrEmpty(p))));\n            }\n        }\n        return joinConjunct(\" or \", spells);\n    }\n\n    private ObjectNode sharedPrerequisites(ArrayNode prerequisites) {\n        ObjectNode shared = prerequisites.objectNode();\n\n        if (prerequisites.size() > 1) {\n            List<JsonNode> others = streamOf(prerequisites).collect(ArrayList::new, ArrayList::add, ArrayList::addAll);\n            // slice(1)\n            JsonNode first = others.get(0);\n            others.remove(0);\n\n            others.stream()\n                    .reduce(first,\n                            (a, b) -> objectIntersect(a, b),\n                            (a, b) -> ((ObjectNode) a).setAll((ObjectNode) b));\n        }\n        return shared;\n    }\n\n    String listPrerequisites(JsonNode variantNode) {\n        List<String> allValues = new ArrayList<>();\n        boolean hasNote = false;\n\n        ArrayNode prerequisites = PrereqFields.prerequisite.readArrayFrom(variantNode);\n\n        // find shared/common prereqs\n        ObjectNode prereqsShared = sharedPrerequisites(prerequisites);\n        String sharedText = prereqsShared.size() > 0\n                ? listPrerequisites(prereqsShared)\n                : null;\n\n        for (JsonNode prerequisite : prerequisites) {\n            List<String> values = new ArrayList<>();\n            String note = null;\n\n            List<PrereqFields> fields = streamOfFieldNames(prerequisite)\n                    .map(x -> {\n                        PrereqFields field = fromString(x);\n                        if (field == PrereqFields.unknown) {\n                            tui().warnf(Msg.UNKNOWN, \"Unexpected prerequisite %s (from %s / %s)\",\n                                    x, prerequisite, getSources().getKey());\n                        }\n                        return field;\n                    })\n                    .sorted()\n                    .toList();\n\n            for (PrereqFields field : fields) {\n                if (prereqsShared.has(field.nodeName())) {\n                    continue;\n                }\n                JsonNode value = field.getFrom(prerequisite);\n\n                switch (field) {\n                    case ability -> values.add(abilityPrereq(value));\n                    case alignment -> values.add(alignmentListToFull(value));\n                    case background -> values.add(backgroundPrereq(value));\n                    case campaign -> values.add(replaceConjoinOr(value, \" Campaign\"));\n                    case culture -> values.add(replaceConjoinOr(value, \" Culture\"));\n                    case exclusiveFeatCategory -> values.add(\"Can't Have Another \" + replaceConjoinOr(value, \" Feat\"));\n                    case expertise -> values.add(expertisePrereq(value));\n                    case feat -> values.add(featPrereq(value));\n                    case featCategory -> values.add(\"Any \" + replaceConjoinOr(value, \" Feat\"));\n                    case feature -> values.add(replaceConjoinOr(value, \" Feature\"));\n                    case optionalfeature -> values.add(replaceConjoinOr(value, \"\"));\n                    case group -> values.add(replaceConjoinOr(value, \" Group\"));\n                    case item -> values.add(replaceConjoinOr(value, \"\"));\n                    case itemProperty -> values.add(itemPropertyPrereq(value));\n                    case itemType -> values.add(itemTypePrereq(value));\n                    case level -> values.add(levelPrereq(value));\n                    case membership -> values.add(\"Membership in the \" + replaceConjoinOr(value, \"\"));\n                    case other -> values.add(replaceText(value));\n                    case otherSummary -> values.add(SourceField.entry.replaceTextFrom(value, this));\n                    case pact -> values.add(\"Pact of the \" + replaceText(value));\n                    case patron -> values.add(replaceText(value + \" Patron\"));\n                    case proficiency -> values.add(proficiencyPrereq(value));\n                    case race -> values.add(racePrereq(value));\n                    case spell -> values.add(spellPrereq(value));\n                    case spellcastingFocus -> values.add(scfPrereq(value));\n                    // --- Boolean values ----\n                    case psionics -> values.addAll(testBoolean(value,\n                            replaceText(\"Psionic Talent feature or {@feat Wild Talent|UA2020PsionicOptionsRevisited} feat\")));\n                    case spellcasting -> values.addAll(testBoolean(value,\n                            \"The ability to cast at least one spell\"));\n                    case spellcasting2020 -> values.addAll(testBoolean(value,\n                            \"Spellcasting or Pact Magic feature\"));\n                    case spellcastingFeature -> values.addAll(testBoolean(value,\n                            \"Spellcasting feature\"));\n                    case spellcastingPrepared -> values.addAll(testBoolean(value,\n                            \"Spellcasting feature from a class that prepares spells\"));\n                    // --- Other: Note ----\n                    case note -> note = replaceText(value);\n                    default -> {\n                        tui().debugf(Msg.UNKNOWN, \"Unexpected prerequisite %s (from %s)\", field.nodeName(), prerequisite);\n                    }\n                }\n            }\n\n            // remove empty values\n            values = values.stream().filter(StringUtil::isPresent).toList();\n\n            hasNote |= isPresent(note);\n            String prereqs = String.join(\n                    values.stream().anyMatch(x -> x.contains(\" or \")) ? \"; \" : \", \",\n                    values);\n            allValues.add(prereqs + (isPresent(note) ? \". \" + note : \"\"));\n        }\n\n        String joinedText = hasNote\n                ? String.join(\" Or, \", allValues)\n                : joinConjunct(allValues.stream().anyMatch(x -> x.contains(\" or \")) ? \"; \" : \", \",\n                        \" or \", allValues);\n\n        return sharedText == null\n                ? joinedText\n                : sharedText + \", plus \" + joinedText;\n\n    }\n\n    ImmuneResist immuneResist() {\n        return new ImmuneResist(\n                collectImmunities(rootNode, VulnerabilityFields.vulnerable),\n                collectImmunities(rootNode, VulnerabilityFields.resist),\n                collectImmunities(rootNode, VulnerabilityFields.immune),\n                collectImmunities(rootNode, VulnerabilityFields.conditionImmune));\n    }\n\n    AbilityScores abilityScores(JsonNode scoreNode) {\n        AbilityScores.Builder builder = new AbilityScores.Builder();\n        builder.setStrength(AbilityScoreFields.str.intOrDefault(rootNode, 10));\n        builder.setDexterity(AbilityScoreFields.dex.intOrDefault(rootNode, 10));\n        builder.setConstitution(AbilityScoreFields.con.intOrDefault(rootNode, 10));\n        builder.setIntelligence(AbilityScoreFields.intel.intOrDefault(rootNode, 10));\n        builder.setWisdom(AbilityScoreFields.wis.intOrDefault(rootNode, 10));\n        builder.setCharisma(AbilityScoreFields.cha.intOrDefault(rootNode, 10));\n        return builder.build();\n    }\n\n    String speed(JsonNode speedNode) {\n        return speed(speedNode, true);\n    }\n\n    String speed(JsonNode speedNode, boolean includeZeroWalk) {\n        if (speedNode == null) {\n            return null;\n        } else if (speedNode.isNumber()) {\n            return String.format(\"%s ft.\", speedNode.asText());\n        }\n        JsonNode alternate = Tools5eFields.alternate.getFrom(speedNode);\n        String note = SourceField.note.replaceTextFrom(speedNode, this);\n\n        List<String> speed = new ArrayList<>();\n        for (String k : SPEED_MODE) {\n            JsonNode v = speedNode.get(k);\n            JsonNode altV = alternate == null ? null : alternate.get(k);\n            if (v != null) {\n                String prefix = \"walk\".equals(k) ? \"\" : k + \" \";\n                speed.add(prefix + speedValue(k, v, includeZeroWalk));\n                if (altV != null && altV.isArray()) {\n                    altV.forEach(x -> speed.add(prefix + speedValue(k, x, includeZeroWalk)));\n                }\n            }\n        }\n        return replaceText(String.join(\", \", speed)\n                + (note.isBlank() ? \"\" : \" \" + note));\n    }\n\n    String speedValue(String key, JsonNode speedValue, boolean includeZeroWalk) {\n        if (speedValue == null || speedValue.isNull()) {\n            if (includeZeroWalk && \"walk\".equals(key)) {\n                return \"0 ft.\";\n            }\n            return \"\";\n        } else if (speedValue.isBoolean() && !\"walk\".equals(key)) {\n            return \"equal to walking speed\";\n        } else if (speedValue.isNumber()) {\n            return String.format(\"%s ft.\", speedValue.asText());\n        } else if (speedValue.isTextual()) { // Varies\n            return speedValue.asText();\n        }\n\n        int number = Tools5eFields.number.intOrDefault(speedValue, 0);\n        if (!includeZeroWalk && number == 0 && \"walk\".equals(key)) {\n            return \"\";\n        }\n        String condition = Tools5eFields.condition.replaceTextFrom(speedValue, this);\n        return String.format(\"%s ft.%s\", number,\n                condition.isEmpty() ? \"\" : \" \" + condition);\n    }\n\n    void findAc(AcHp acHp) {\n        JsonNode acNode = MonsterFields.ac.getFrom(rootNode);\n        if (acNode == null) {\n            return;\n        }\n        if (acNode.isIntegralNumber()) {\n            acHp.ac = acNode.asInt();\n        } else if (acNode.isArray()) {\n            List<String> details = new ArrayList<>();\n            for (JsonNode acValue : iterableElements(acNode)) {\n                if (acHp.ac == null && details.isEmpty()) { // first time\n                    if (acValue.isIntegralNumber()) {\n                        acHp.ac = acValue.asInt();\n                    } else if (acValue.isObject()) {\n                        if (MonsterFields.ac.existsIn(acValue)) {\n                            acHp.ac = MonsterFields.ac.getFrom(acValue).asInt();\n                        }\n                        if (MonsterFields.special.existsIn(acValue)) {\n                            details.add(MonsterFields.special.replaceTextFrom(acValue, this));\n                        } else if (MonsterFields.from.existsIn(acValue)) {\n                            details.add(joinAndReplace(MonsterFields.from.readArrayFrom(acValue)));\n                        }\n                    }\n                } else { // nth time: conditional AC. Append to acText\n                    StringBuilder value = new StringBuilder();\n                    value.append(MonsterFields.ac.replaceTextFrom(acValue, this));\n                    if (MonsterFields.from.existsIn(acValue)) {\n                        value.append(\" \").append(joinAndReplace(MonsterFields.from.readArrayFrom(acValue)));\n                    }\n                    if (Tools5eFields.condition.existsIn(acValue)) {\n                        value.append(\" \").append(Tools5eFields.condition.replaceTextFrom(acValue, this));\n                    }\n                    details.add(value.toString());\n                }\n            }\n            if (!details.isEmpty()) {\n                acHp.acText = replaceText(String.join(\"; \", details));\n            }\n        } else if (MonsterFields.special.existsIn(acNode)) {\n            acHp.acText = MonsterFields.special.replaceTextFrom(acNode, this);\n        } else {\n            tui().warnf(Msg.UNKNOWN, \"Unknown armor class in monster %s: %s\", sources.getKey(), acNode.toPrettyString());\n        }\n    }\n\n    void findHp(AcHp acHp) {\n        JsonNode health = MonsterFields.hp.getFrom(rootNode);\n\n        if (health != null && health.isNumber()) {\n            acHp.hp = health.asInt();\n        } else if (MonsterFields.special.existsIn(health)) {\n            String special = MonsterFields.special.replaceTextFrom(health, this);\n            if (special.matches(\"^[\\\\d\\\"]+$\")) {\n                acHp.hp = Integer.parseInt(special.replace(\"\\\"\", \"\"));\n                if (MonsterFields.original.existsIn(health)) {\n                    acHp.hpText = MonsterFields.original.replaceTextFrom(health, this);\n                }\n            } else {\n                acHp.hpText = replaceText(special);\n            }\n        } else {\n            if (MonsterFields.average.existsIn(health)) {\n                acHp.hp = MonsterFields.average.getFrom(health).asInt();\n            }\n            if (MonsterFields.formula.existsIn(health)) {\n                acHp.hitDice = MonsterFields.formula.getFrom(health).asText();\n            }\n        }\n\n        if (acHp.hpText == null && acHp.hitDice == null && acHp.hp == null) {\n            acHp.hp = null;\n            acHp.hpText = \"—\";\n        }\n    }\n\n    ImageRef getToken() {\n        // 5eTools mirror 1 - png; differences in path construction\n        //\n        // static getTokenUrl (obj) {\n        //     return obj.tokenUrl\n        //       || UrlUtil.link(`${Renderer.get().baseMediaUrls[\"img\"]\n        //       || Renderer.get().baseUrl}img/objects/tokens/${Parser.sourceJsonToAbv(obj.source)}/${Parser.nameToTokenName(obj.name)}.png`);\n        // }\n        //          Renderer.get().baseUrl}img/${Parser.sourceJsonToAbv(mon.source)}/${Parser.nameToTokenName(mon.name)}.png`);\n        //          Renderer.get().baseUrl}img/vehicles/tokens/${Parser.sourceJsonToAbv(veh.source)}/${Parser.nameToTokenName(veh.name)}.png`);\n        //\n        // nameToTokenName = function (name) { return name.toAscii().replace(/\"/g, \"\"); }\n        //\n        // 5eTools mirror 2 - webp\n        //\n        // Notice injection of base path (img) into rendered URL\n        // static getTokenUrl (mon) {\n        //     if (mon.tokenUrl) return mon.tokenUrl;\n        //     return Renderer.get().getMediaUrl(\"img\",\n        //          `bestiary/tokens/${Parser.sourceJsonToAbv(mon.source)}/${Parser.nameToTokenName(mon.name)}.webp`);\n        // }\n        //          `objects/tokens/${Parser.sourceJsonToAbv(obj.source)}/${Parser.nameToTokenName(obj.name)}.webp`\n        //          `vehicles/tokens/${Parser.sourceJsonToAbv(veh.source)}/${Parser.nameToTokenName(veh.name)}.webp`\n        // this.getMediaUrl = function (mediaDir, path) {\n        //      if (Renderer.get().baseMediaUrls[mediaDir])\n        //          return `${Renderer.get().baseMediaUrls[mediaDir]}${path}`;\n        //      return `${Renderer.get().baseUrl}${mediaDir}/${path}`;\n        // };\n        String targetDir = getImagePath() + \"/token/\";\n\n        // \"original\" is set by conjured monster variant\n        String name = getTextOrDefault(rootNode, \"original\", getName());\n        String filename = Normalizer.normalize(name, Form.NFD)\n                .replaceAll(\"\\\\p{M}\", \"\")\n                .replace(\"Æ\", \"AE\")\n                .replace(\"æ\", \"ae\")\n                .replace(\"\\\"\", \"\");\n\n        String sourcePath;\n        if (Tools5eFields.tokenHref.existsIn(rootNode)) {\n            JsonHref href = readHref(Tools5eFields.tokenHref.getFrom(rootNode));\n            sourcePath = href.url == null ? href.path : href.url;\n        } else {\n            sourcePath = Tools5eFields.tokenUrl.getTextOrNull(rootNode);\n        }\n\n        final String ext = sourcePath == null\n                ? \".webp\"\n                : sourcePath.substring(sourcePath.lastIndexOf('.'));\n\n        if (sourcePath == null && Tools5eFields.hasToken.booleanOrDefault(rootNode, false)) {\n            // Construct the source path\n            List<String> paths = new ArrayList<>();\n            switch (type) {\n                case monster -> paths.add(\"bestiary/tokens\");\n                case object -> paths.add(\"objects/tokens\");\n                case vehicle -> paths.add(\"vehicles/tokens\");\n                default -> throw new IllegalArgumentException(\"Unknown type looking for token path: \" + type);\n            }\n            paths.add(getSources().mapPrimarySource());\n            paths.add(filename + ext);\n\n            sourcePath = (String.join(\"/\", paths)).replace(\"//\", \"/\");\n        }\n\n        if (sourcePath == null) {\n            return null;\n        }\n\n        Path targetFile = Path.of(targetDir,\n                linkifier().getTargetFileName(slugify(filename), getSources()) + ext);\n\n        return getSources().buildTokenImageRef(index, sourcePath, targetFile, true);\n    }\n\n    String collectImmunities(JsonNode fromNode, VulnerabilityFields field) {\n        if (field.existsIn(fromNode)) { // filter out null elements\n            List<String> immunities = new ArrayList<>();\n            StringBuilder separator = new StringBuilder();\n\n            for (JsonNode value : field.iterateArrayFrom(fromNode)) {\n                if (value.isTextual()) { // damage or condition type\n                    immunities.add(textValue(field, value.asText()));\n                } else if (VulnerabilityFields.special.existsIn(value)) { // \"special\"\n                    immunities.add(VulnerabilityFields.special.replaceTextFrom(value, this)\n                            .replace(\"see (below|above)\", \"see details\"));\n                } else { // conditional\n                    List<String> allText = new ArrayList<>();\n                    if (VulnerabilityFields.preNote.existsIn(value)) {\n                        allText.add(VulnerabilityFields.preNote.replaceTextFrom(value, this));\n                    }\n                    allText.add(collectImmunities(value, field));\n                    if (VulnerabilityFields.note.existsIn(value)) {\n                        allText.add(VulnerabilityFields.note.replaceTextFrom(value, this));\n                    }\n                    if (separator.length() == 0 && !allText.isEmpty()) {\n                        separator.append(\"; \");\n                        immunities.add(String.join(\" \", allText));\n                    }\n                }\n            }\n\n            if (separator.length() == 0) {\n                separator.append(\", \");\n            }\n            return String.join(separator.toString(), immunities);\n        }\n        return null;\n    }\n\n    private String textValue(VulnerabilityFields field, String text) {\n        if (field == VulnerabilityFields.conditionImmune) {\n            return linkify(Tools5eIndexType.condition, text);\n        }\n        return text;\n    }\n\n    List<NamedText> collectSortedTraits(JsonNode array) {\n        boolean pushed = parseState().pushTrait();\n        try {\n            // gather traits into a sorted array\n            ArrayNode sorted = Tui.MAPPER.createArrayNode();\n            sorted.addAll(sortedTraits(array));\n\n            List<NamedText> namedText = new ArrayList<>();\n            collectTraits(namedText, sorted);\n            return namedText;\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    List<NamedText> collectTraits(String field) {\n        boolean pushed = parseState().pushTrait();\n        try {\n            List<NamedText> traits = new ArrayList<>();\n            JsonNode header = rootNode.get(field + \"Header\");\n            if (header != null) {\n                addNamedTrait(traits, \"\", header);\n            }\n            collectTraits(traits, rootNode.get(field));\n            return traits;\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    void collectTraits(List<NamedText> traits, JsonNode array) {\n        boolean pushed = parseState().pushTrait();\n        try {\n            if (array == null || array.isNull()) {\n                return;\n            } else if (array.isObject()) {\n                tui().warnf(Msg.UNKNOWN, \"Unknown %s for %s: %s\", array, sources.getKey(), array.toPrettyString());\n                return;\n            }\n            if (streamOf(array).allMatch(e -> e.isObject() && SourceField.name.existsIn(e))) {\n                for (JsonNode e : iterableElements(array)) {\n                    String name = SourceField.name.replaceTextFrom(e, this)\n                            .replaceAll(\":$\", \"\");\n                    addNamedTrait(traits, name, e);\n                }\n            } else {\n                // no names, just text\n                List<String> text = new ArrayList<>();\n                appendToText(text, array, null);\n                if (!text.isEmpty()) {\n                    traits.add(new NamedText(\"\", text, List.of()));\n                }\n            }\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    void addNamedTrait(List<NamedText> traits, String name, JsonNode node) {\n        boolean pushed = parseState().pushTrait();\n        try {\n            List<String> text = new ArrayList<>();\n            List<NamedText> nested = List.of();\n            if (node.isObject()) {\n                if (!SourceField.name.existsIn(node)) {\n                    appendToText(text, node, null);\n                } else {\n                    appendToText(text, SourceField.entry.getFrom(node), null);\n                    appendToText(text, SourceField.entries.getFrom(node), null);\n                }\n            } else if (node.isArray()) {\n                // preformat text, but also collect nodes\n                appendToText(text, node, null);\n                nested = new ArrayList<>();\n                collectTraits(nested, node);\n            } else {\n                appendToText(text, node, null);\n            }\n            NamedText nt = new NamedText(name, text, nested);\n            if (nt.hasContent()) {\n                traits.add(nt);\n            }\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    List<String> collectEntries(JsonNode node) {\n        List<String> text = new ArrayList<>();\n        appendToText(text, SourceField.entry.getFrom(node), null);\n        appendToText(text, SourceField.entries.getFrom(node), null);\n        return text;\n    }\n\n    List<JsonNode> sortedTraits(JsonNode arrayNode) {\n        boolean pushed = parseState().pushTrait();\n        try {\n            if (arrayNode == null || arrayNode.isNull()) {\n                return List.of();\n            } else if (arrayNode.isObject()) {\n                tui().errorf(\"Can't sort an object: %s\", arrayNode);\n                throw new IllegalArgumentException(\"Object passed to sortedTraits: \" + getSources());\n            }\n\n            return streamOf(arrayNode).sorted((a, b) -> {\n                Optional<Integer> aSort = Tools5eFields.sort.intFrom(a);\n                Optional<Integer> bSort = Tools5eFields.sort.intFrom(b);\n\n                if (aSort.isPresent() && bSort.isPresent()) {\n                    return aSort.get().compareTo(bSort.get());\n                } else if (aSort.isPresent() && bSort.isEmpty()) {\n                    return -1;\n                } else if (aSort.isEmpty() && bSort.isPresent()) {\n                    return 1;\n                }\n\n                String aName = SourceField.name.replaceTextFrom(a, this).toLowerCase();\n                String bName = SourceField.name.replaceTextFrom(b, this).toLowerCase();\n                if (aName.isEmpty() && bName.isEmpty()) {\n                    return 0;\n                }\n\n                boolean isOnlyA = aName.endsWith(\" only)\");\n                boolean isOnlyB = bName.endsWith(\" only)\");\n                if (!isOnlyA && isOnlyB) {\n                    return -1;\n                } else if (isOnlyA && !isOnlyB) {\n                    return 1;\n                }\n\n                int specialA = specialTraits.indexOf(aName);\n                int specialB = specialTraits.indexOf(bName);\n                if (specialA > -1 && specialB > -1) {\n                    return specialA - specialB;\n                } else if (specialA > -1 && specialB == -1) {\n                    return -1;\n                } else if (specialA == -1 && specialB > -1) {\n                    return 1;\n                }\n\n                return aName.compareTo(bName);\n            }).toList();\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    public final Tools5eQuteBase build() {\n        boolean pushed = parseState().push(getSources(), rootNode);\n        try {\n            return buildQuteResource();\n        } catch (Exception e) {\n            tui().errorf(e, \"build(): Error processing '%s': %s\", getName(), e.toString());\n            throw e;\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    public final Tools5eQuteNote buildNote() {\n        boolean pushed = parseState().push(getSources(), rootNode);\n        try {\n            return buildQuteNote();\n        } catch (Exception e) {\n            tui().errorf(e, \"buildNote(): Error processing '%s': %s\", getName(), e.toString());\n            throw e;\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    protected Tools5eQuteBase buildQuteResource() {\n        tui().warnf(\"The default buildQuteResource method was called for %s. Was this intended?\", sources.toString());\n        return null;\n    }\n\n    protected Tools5eQuteNote buildQuteNote() {\n        tui().warnf(\"The default buildQuteNote method was called for %s. Was this intended?\", sources.toString());\n        return null;\n    }\n\n    enum VulnerabilityFields implements JsonNodeReader {\n        cond,\n        conditionImmune,\n        immune,\n        note,\n        preNote,\n        resist,\n        special,\n        vulnerable,\n    }\n\n    // weighted (order matters)\n    enum PrereqFields implements JsonNodeReader {\n        /* */ level,\n        /* */ pact,\n        /* */ patron,\n        /* */ spell,\n        /* */ race,\n        /* */ alignment,\n        /* */ ability,\n        /* */ proficiency,\n        /* */ expertise,\n        /* */ spellcasting,\n        /* */ spellcasting2020,\n        /* */ spellcastingFeature,\n        /* */ spellcastingPrepared,\n        /* */ spellcastingFocus,\n        /* */ psionics,\n        /* */ feature,\n        /* */ feat,\n        /* */ background,\n        /* */ item,\n        /* */ itemType,\n        /* */ itemProperty,\n        /* */ campaign,\n        /* */ culture,\n        /* */ group,\n        /* */ membership,\n        /* */ other,\n        /* */ otherSummary,\n        /* */ exclusiveFeatCategory,\n        /* */ featCategory,\n        choose, // inner field for spells\n        displayEntry, // inner field for display\n        note, // field alongside other fields\n        prerequisite, // prereq field itself\n        optionalfeature,\n        unknown // catcher for unknown attributes (see #fromString())\n    }\n\n    static PrereqFields fromString(String name) {\n        for (PrereqFields f : PrereqFields.values()) {\n            if (f.name().equals(name)) {\n                return f;\n            }\n        }\n        return PrereqFields.unknown;\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteCompose.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.List;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.ArrayNode;\n\nimport dev.ebullient.convert.config.TtrpgConfig;\nimport dev.ebullient.convert.tools.JsonNodeReader;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.ToolsIndex.TtrpgValue;\nimport dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteNote;\n\npublic class Json2QuteCompose extends Json2QuteCommon {\n    final List<JsonNode> nodes = new ArrayList<>();\n    final List<Tools5eQuteNote> splitNotes = new ArrayList<>();\n    Tools5eSources currentSources;\n    final String title;\n    final String targetPath;\n\n    public Json2QuteCompose(Tools5eIndexType type, Tools5eIndex index, String title) {\n        this(type, index, title, \".\");\n    }\n\n    public Json2QuteCompose(Tools5eIndexType type, Tools5eIndex index, String title, String targetPath) {\n        super(index, type, null);\n        this.title = title;\n        this.targetPath = targetPath;\n    }\n\n    public void add(JsonNode node) {\n        String key = TtrpgValue.indexKey.getTextOrEmpty(node);\n        if (index.isIncluded(key)) {\n            nodes.add(node);\n        } else {\n            tui().debugf(\"%s: %s is excluded\", type.name(), key);\n        }\n    }\n\n    @Override\n    public String getName() {\n        return title;\n    }\n\n    @Override\n    public Tools5eSources getSources() {\n        return currentSources;\n    }\n\n    public List<Tools5eQuteNote> getSplitNotes() {\n        return Collections.unmodifiableList(splitNotes);\n    }\n\n    @Override\n    protected Tools5eQuteNote buildQuteNote() {\n        if (nodes.isEmpty()) {\n            return null;\n        }\n\n        boolean split = isSplittable() && TtrpgConfig.getConfig().splitRules();\n        String subfolder = split\n                ? Tools5eLinkifier.instance().getRelativePath(type)\n                : null;\n\n        Tags tags = new Tags();\n        List<String> text = new ArrayList<>();\n\n        if (type == Tools5eIndexType.itemType || type == Tools5eIndexType.itemProperty) {\n            nodes.forEach(x -> flatten(x));\n        }\n\n        nodes.sort(Comparator.comparing(SourceField.name::getTextOrEmpty));\n\n        if (type == Tools5eIndexType.itemProperty) {\n            appendItemProperties(text, tags);\n        } else if (split) {\n            for (JsonNode entry : nodes) {\n                appendSplitElement(entry, text, tags, subfolder);\n            }\n        } else {\n            for (JsonNode entry : nodes) {\n                appendElement(entry, text, tags);\n            }\n        }\n\n        return new Tools5eQuteNote(title, null, text, tags)\n                .withTargetPath(split ? subfolder : targetPath);\n    }\n\n    /** Types that can be split into individual notes */\n    private boolean isSplittable() {\n        return switch (type) {\n            case action, condition, disease, itemMastery, sense, skill, status -> true;\n            default -> false;\n        };\n    }\n\n    /** Tag segment for the individual note's type tag */\n    private String typeTag() {\n        return switch (type) {\n            case action -> \"action\";\n            case condition, status -> \"condition\";\n            case disease -> \"disease\";\n            case itemMastery -> \"item/mastery\";\n            case sense -> \"sense\";\n            case skill -> \"skill\";\n            default -> type.name();\n        };\n    }\n\n    private void appendSplitElement(JsonNode entry, List<String> text, Tags tags, String subfolder) {\n        currentSources = Tools5eSources.findOrTemporary(entry);\n\n        boolean pushed = parseState().push(entry);\n        try {\n            String name = SourceField.name.replaceTextFrom(entry, index);\n\n            // Add source tags to collated doc\n            tags.addSourceTags(currentSources);\n\n            // Build individual note content\n            Tags noteTags = new Tags(currentSources);\n            noteTags.addRaw(typeTag());\n            List<String> noteText = new ArrayList<>();\n\n            noteText.add(getLabeledSource(entry));\n\n            if (type == Tools5eIndexType.action) {\n                appendAction(entry, noteText);\n            } else if (entry.has(\"table\")) {\n                appendTable(name, entry, noteText);\n            } else {\n                appendToText(noteText, entry, null);\n            }\n\n            Tools5eQuteNote note = new Tools5eQuteNote(name, null, noteText, noteTags)\n                    .withTargetPath(subfolder);\n            splitNotes.add(note);\n\n            // Add embed line to collated doc\n            String fileName = slugify(name) + \".md\";\n            maybeAddBlankLine(text);\n            text.add(\"## \" + name);\n            text.add(\"\");\n            text.add(\"![%s](%s)\".formatted(name, fileName));\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    private void appendElement(JsonNode entry, List<String> text, Tags tags) {\n        currentSources = Tools5eSources.findOrTemporary(entry);\n\n        boolean pushed = parseState().push(entry);\n        try {\n            String abbreviation = Tools5eFields.abbreviation.getTextOrNull(entry);\n            String name = SourceField.name.replaceTextFrom(entry, index);\n\n            tags.addSourceTags(currentSources);\n\n            maybeAddBlankLine(text);\n            text.add(\"## \" + name);\n            text.add(getLabeledSource(entry));\n\n            if (type == Tools5eIndexType.action) {\n                appendAction(entry, text);\n            } else if (entry.has(\"table\")) {\n                appendTable(name, entry, text);\n            } else {\n                appendToText(text, entry, null);\n            }\n\n            if (type == Tools5eIndexType.itemType && abbreviation != null) {\n                List<JsonNode> more = index.elementsMatching(Tools5eIndexType.itemTypeAdditionalEntries, abbreviation);\n                for (JsonNode m : more) {\n                    appendToText(text, m, \"###\");\n                }\n            }\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    private void flatten(JsonNode entry) {\n        if (SourceField.name.existsIn(entry) && SourceField.entries.existsIn(entry)) {\n            return;\n        }\n        JsonNode next = SourceField.entries.existsIn(entry)\n                ? SourceField.entries.getFrom(entry)\n                : Tools5eFields.entriesTemplate.getFrom(entry);\n        if (SourceField.name.existsIn(entry) && !SourceField.entries.existsIn(entry)) {\n            // entries did not exist (probably because it was a template instead)\n            SourceField.entries.copy(next, entry);\n            return;\n        }\n        JsonNode content = next == null ? null : next.get(0);\n        if (SourceField.name.existsIn(content) && SourceField.entries.existsIn(content)) {\n            SourceField.name.copy(content, entry);\n            SourceField.entries.copy(content, entry);\n            return;\n        }\n        tui().warnf(\"Unable to flatten entry from %s. Content may be missing. %s\",\n                Tools5eSources.findSources(entry), entry);\n    }\n\n    private void appendTable(String name, JsonNode entry, List<String> text) {\n        String blockid = slugify(name);\n\n        maybeAddBlankLine(text);\n        text.add(String.format(\"`dice: [](%s.md#^%s)`\", slugify(title), blockid));\n        text.add(\"\");\n\n        ArrayNode table = entry.withArray(\"table\");\n        if (table.get(0).isTextual()) {\n            String header = \"| \" + name + \" |\";\n            text.add(header);\n            text.add(header.replaceAll(\"[^|]\", \"-\"));\n            table.forEach(x -> text.add(\"| \" + replaceText(x.asText()) + \" |\"));\n        } else {\n            String header = \"| dice: d100 | \" + name + \" |\";\n            text.add(header);\n            text.add(header.replaceAll(\"[^|]\", \"-\"));\n            table.forEach(r -> {\n                String item;\n                if (r.has(\"item\")) {\n                    item = replaceText(r.get(\"item\").asText());\n                } else if (r.has(\"choose\")) {\n                    item = \"Choose: \" + chooseFrom(r.get(\"choose\"));\n                } else {\n                    throw new IllegalArgumentException(\"What kind of item? \" + r.toPrettyString());\n                }\n\n                if (r.has(\"min\") && r.has(\"max\")) {\n                    int min = r.get(\"min\").asInt();\n                    int max = r.get(\"max\").asInt();\n                    if (min == max) {\n                        text.add(String.format(\"| %s | %s |\", min, item));\n                    } else {\n                        text.add(String.format(\"| %s-%s | %s |\", min, max, item));\n                    }\n                }\n            });\n        }\n        text.add(\"^\" + blockid);\n    }\n\n    String chooseFrom(JsonNode choose) {\n        // TODO: Replace generic/group lists\n        if (choose.has(\"fromGroup\")) {\n            return joinAndReplace(choose.withArray(\"fromGroup\"));\n        } else if (choose.has(\"fromGeneric\")) {\n            return joinAndReplace(choose.withArray(\"fromGeneric\"));\n        } else if (choose.has(\"fromItems\")) {\n            // TODO: Another/following table!\n            return joinAndReplace(choose.withArray(\"fromItems\"));\n        }\n        throw new IllegalArgumentException(\"What kind of item to choose? \" + choose.toPrettyString());\n    }\n\n    private void appendAction(JsonNode entry, List<String> text) {\n\n        String duration = flattenActionTime(ComposedTypeFields.time.getFrom(entry));\n        if (!duration.isEmpty()) {\n            maybeAddBlankLine(text);\n            text.add(\"- **Duration**: \" + duration);\n        }\n\n        maybeAddBlankLine(text);\n        appendToText(text, entry, \"###\");\n\n        List<String> seeAlso = ComposedTypeFields.seeAlsoAction.linkifyListFrom(entry, Tools5eIndexType.action, index);\n        if (!seeAlso.isEmpty()) {\n            maybeAddBlankLine(text);\n            text.add(\"See also: \" + String.join(\", \", seeAlso));\n        }\n\n        String fromVariant = ComposedTypeFields.fromVariant.getTextOrNull(entry);\n        if (fromVariant != null) {\n            maybeAddBlankLine(text);\n            text.add(\"This action is an optional addition to the game, from the optional/variant rule \"\n                    + linkifyVariantRule(fromVariant) + \".\");\n        }\n    }\n\n    private String flattenActionTime(JsonNode entry) {\n        if (entry == null || entry.isNull()) {\n            return \"\";\n        } else if (entry.isTextual()) {\n            return entry.asText();\n        } else if (entry.isObject()) {\n            return String.format(\"%s %s\", ComposedTypeFields.number.replaceTextFrom(entry, index),\n                    ComposedTypeFields.unit.replaceTextFrom(entry, index));\n        } else {\n            List<String> elements = new ArrayList<>();\n            entry.forEach(x -> elements.add(flattenActionTime(x)));\n            return String.join(\", \", elements);\n        }\n    }\n\n    private void appendItemProperties(List<String> text, Tags tags) {\n        final JsonNode srdEntries = TtrpgConfig.activeGlobalConfig(\"srdEntries\").get(\"properties\");\n\n        for (JsonNode srdEntry : iterableElements(srdEntries)) {\n            String finalKey = TtrpgValue.indexKey.getTextOrEmpty(srdEntry);\n            // Use isExcluded without following reprint aliases:\n            // reprinted entries should be skipped (the newer version will be included separately)\n            if (!index().isIncluded(finalKey, false)) {\n                continue;\n            }\n            currentSources = Tools5eSources.findSources(finalKey);\n\n            boolean p2 = parseState().push(srdEntry);\n            try {\n                String name = currentSources.getName();\n\n                maybeAddBlankLine(text);\n                text.add(\"## \" + name);\n                if (!currentSources.isSrdOrBasicRules()) {\n                    text.add(getLabeledSource(srdEntry));\n                }\n                text.add(\"\");\n\n                if (name.equals(\"General and Weapon Properties\")) {\n                    List<JsonNode> sorted = nodes.stream()\n                            .filter(this::propertyIncluded)\n                            .sorted(Comparator.comparing(SourceField.name::getTextOrEmpty))\n                            .toList();\n\n                    for (JsonNode property : sorted) {\n                        String propName = SourceField.name.getTextOrEmpty(property);\n                        if (propName.equalsIgnoreCase(\"special\")) {\n                            continue;\n                        }\n                        maybeAddBlankLine(text);\n                        text.add(\"### \" + propName);\n                        if (!Tools5eSources.isSrd(property)) {\n                            text.add(getLabeledSource(property));\n                        }\n                        List<String> inner = new ArrayList<>();\n                        appendToText(inner, SourceField.entries.getFrom(property), null);\n                        if (!inner.isEmpty()) {\n                            inner.set(0, inner.get(0).replaceAll(\"^\\\\*\\\\*.*?\\\\.\\\\*\\\\* \", \"\"));\n                            text.addAll(inner);\n                        }\n                    }\n                } else {\n                    appendToText(text, SourceField.entries.getFrom(srdEntry), \"###\");\n                }\n            } finally {\n                parseState().pop(p2);\n            }\n        }\n    }\n\n    private boolean propertyIncluded(JsonNode x) {\n        Tools5eSources sources = Tools5eSources.findSources(x);\n        if (sources != null) {\n            return sources.includedByConfig();\n        }\n        String source = SourceField.source.getTextOrEmpty(x);\n        return index.sourceIncluded(source);\n    }\n\n    enum ComposedTypeFields implements JsonNodeReader {\n        fromGeneric,\n        fromGroup,\n        fromItems,\n        fromVariant,\n        number,\n        seeAlsoAction,\n        time,\n        unit\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteDeck.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static dev.ebullient.convert.StringUtil.toTitleCase;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Optional;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.qute.SourceAndPage;\nimport dev.ebullient.convert.tools.JsonNodeReader;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteDeck;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteDeck.Card;\nimport dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase;\n\npublic class Json2QuteDeck extends Json2QuteCommon {\n\n    Json2QuteDeck(Tools5eIndex index, Tools5eIndexType type, JsonNode jsonNode) {\n        super(index, type, jsonNode);\n    }\n\n    @Override\n    protected Tools5eQuteBase buildQuteResource() {\n        Tags tags = new Tags(sources);\n        List<String> text = new ArrayList<>();\n        List<Card> cards = new ArrayList<>();\n\n        appendToText(text, rootNode, \"##\");\n\n        boolean hasCardArt = DeckFields.hasCardArt.booleanOrDefault(rootNode, false);\n        for (JsonNode cardRef : DeckFields.cards.iterateArrayFrom(rootNode)) {\n            final String cardKey;\n            if (cardRef.isTextual()) {\n                cardKey = Tools5eIndexType.card.fromTagReference(cardRef.asText());\n            } else if (cardRef.isObject()) {\n                cardKey = Tools5eIndexType.card.fromTagReference(DeckFields.uid.getTextOrThrow(cardRef));\n            } else {\n                cardKey = null;\n            }\n\n            if (cardKey != null) {\n                JsonNode cardNode = index.getNode(cardKey);\n                if (cardNode == null) {\n                    tui().errorf(\"Unable to find %s referenced from %s\", cardKey, sources.getKey());\n                } else {\n                    appendCard(hasCardArt, cards, cardNode);\n                }\n            }\n        }\n\n        return new QuteDeck(sources,\n                getName(),\n                getSourceText(sources),\n                getImage(DeckFields.back, rootNode),\n                cards,\n                String.join(\"\\n\", text),\n                tags);\n    }\n\n    public void appendCard(boolean hasCardArt, List<Card> cards, JsonNode cardNode) {\n        String name = SourceField.name.getTextOrEmpty(cardNode);\n        ImageRef face = hasCardArt ? getImage(DeckFields.face, cardNode) : null;\n        String cardText = flattenToString(cardNode);\n        String suit = DeckFields.suit.getTextOrEmpty(cardNode);\n        Optional<Integer> value = DeckFields.value.intFrom(cardNode);\n        String valueName = DeckFields.valueName.getTextOrEmpty(cardNode);\n\n        String suitValue = null;\n        if (!suit.isEmpty() && (value.isPresent() || !valueName.isEmpty())) {\n            suitValue = toTitleCase(valueName.isEmpty() ? numberToText(value.get()) : valueName);\n            suitValue += \" of \" + toTitleCase(suit);\n\n            if (!suitValue.toLowerCase().equals(name.toLowerCase())) {\n                cardText = \"*\" + suitValue + \"*\\n\\n\" + cardText;\n            }\n        }\n        cards.add(new Card(name, face, cardText, suitValue, new SourceAndPage(cardNode)));\n    }\n\n    ImageRef getImage(JsonNodeReader field, JsonNode imgSource) {\n        JsonNode imageRef = field.getFrom(imgSource);\n        if (imageRef != null) {\n            try {\n                JsonMediaHref mediaHref = mapper().treeToValue(imageRef, JsonMediaHref.class);\n                return buildImageRef(mediaHref, getImagePath());\n            } catch (JsonProcessingException | IllegalArgumentException e) {\n                tui().errorf(\"Unable to read media reference from %s\", imageRef.toPrettyString());\n            }\n        }\n        return null;\n    }\n\n    enum DeckFields implements JsonNodeReader {\n        back,\n        cards,\n        face,\n        set,\n        suit,\n        uid,\n        value,\n        valueName,\n        hasCardArt;\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteDeity.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.ArrayNode;\n\nimport dev.ebullient.convert.config.ReprintBehavior;\nimport dev.ebullient.convert.config.TtrpgConfig;\nimport dev.ebullient.convert.io.Msg;\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.tools.JsonNodeReader;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.Tools5eIndex.Tuple;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteDeity;\nimport dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase;\n\npublic class Json2QuteDeity extends Json2QuteCommon {\n\n    Json2QuteDeity(Tools5eIndex index, Tools5eIndexType type, JsonNode jsonNode) {\n        super(index, type, jsonNode);\n    }\n\n    @Override\n    protected Tools5eQuteBase buildQuteResource() {\n        Tags tags = new Tags(getSources());\n\n        String pantheon = getTextOrDefault(rootNode, \"pantheon\", null);\n        if (pantheon != null) {\n            tags.add(\"deity\", pantheon);\n        }\n\n        List<String> domains = new ArrayList<>();\n        if (rootNode.has(\"domains\")) {\n            rootNode.withArray(\"domains\").forEach(d -> {\n                String domain = d.asText();\n                tags.add(\"domain\", domain);\n                domains.add(domain);\n            });\n        }\n\n        return new QuteDeity(sources,\n                getName(),\n                getSourceText(getSources()),\n                DeityField.altNames.replaceTextFromList(rootNode, this),\n                pantheon,\n                dietyAlignment(),\n                replaceText(DeityField.title.getTextOrEmpty(rootNode)),\n                replaceText(DeityField.category.getTextOrEmpty(rootNode)),\n                String.join(\", \", domains),\n                replaceText(DeityField.province.getTextOrEmpty(rootNode)),\n                replaceText(DeityField.symbol.getTextOrEmpty(rootNode)),\n                getSymbolImage(),\n                getText(\"##\"),\n                tags);\n    }\n\n    String dietyAlignment() {\n        ArrayNode a1 = rootNode.withArray(\"alignment\");\n        if (a1.size() == 0) {\n            return \"Unaligned\";\n        }\n        String choices = a1.toString().replaceAll(\"[^LCNEGAUXY]\", \"\");\n        return mapAlignmentToString(choices);\n    }\n\n    ImageRef getSymbolImage() {\n        if (rootNode.has(\"symbolImg\")) {\n            JsonNode symbolImg = rootNode.get(\"symbolImg\");\n            try {\n                JsonMediaHref mediaHref = mapper().treeToValue(symbolImg, JsonMediaHref.class);\n                return buildImageRef(mediaHref, getImagePath());\n            } catch (JsonProcessingException | IllegalArgumentException e) {\n                tui().errorf(\"Unable to read media reference from %s\", symbolImg.toPrettyString());\n            }\n        }\n        return null;\n    }\n\n    public static Iterable<String> findDeities(List<Tuple> allDeities) {\n        var config = TtrpgConfig.getConfig();\n        if (config.reprintBehavior() == ReprintBehavior.all) {\n            return allDeities.stream()\n                    .filter(t -> Tools5eSources.includedByConfig(t.key))\n                    .peek(t -> Tui.instance().logf(Msg.DEITY, \" ----  %s\", t.key))\n                    .map(t -> t.key)\n                    .toList();\n        }\n\n        final Comparator<String> byDate = Comparator\n                .comparing(k -> TtrpgConfig.sourcePublicationDate(k));\n\n        Function<Tuple, String> deityKey = n -> {\n            String name = DeityField.reprintAlias.getTextOrDefault(n.node, SourceField.name.getTextOrEmpty(n.node));\n            String pantheon = DeityField.pantheon.getTextOrEmpty(n.node);\n            return (name + \"-\" + pantheon).toLowerCase();\n        };\n\n        // Group by source\n        Map<String, List<Tuple>> deityBySource = allDeities.stream()\n                .collect(Collectors.groupingBy(t -> SourceField.source.getTextOrEmpty(t.node)));\n\n        // Sort the sources by date, descending\n        List<String> sourcesByDate = deityBySource.keySet().stream()\n                .sorted(byDate.reversed())\n                .toList();\n\n        Map<String, Tuple> keepers = new HashMap<>();\n        // Iterate over groups of deities in order of publication.\n        // Keep the first deity of each name, add others to the remove pile.\n        for (String book : sourcesByDate) {\n            List<Tuple> deities = deityBySource.remove(book);\n\n            if (keepers.isEmpty()) { // most recent bucket. Keep all.\n                deities.forEach(tuple -> {\n                    String key = deityKey.apply(tuple);\n                    if (Tools5eSources.includedByConfig(tuple.key)) {\n                        Tui.instance().logf(Msg.DEITY, \" ----  %60s :: %s\", tuple.key, key);\n                        keepers.put(key, tuple);\n                    } else {\n                        Tui.instance().logf(Msg.DEITY, \"(drop) %s\", tuple.key);\n                    }\n                });\n                continue;\n            }\n            for (Tuple tuple : deities) {\n                String key = deityKey.apply(tuple);\n                if (Tools5eSources.includedByConfig(tuple.key)) {\n                    if (keepers.containsKey(key)) {\n                        Tui.instance().logf(Msg.DEITY, \"(drop | superseded) %47s => %s\", tuple.key, key);\n                    } else {\n                        keepers.put(key, tuple);\n                        Tui.instance().logf(Msg.DEITY, \" ----  %60s :: %s\", tuple.key, key);\n                    }\n                } else {\n                    Tui.instance().logf(Msg.DEITY, \"(drop) %s\", tuple.key);\n                }\n            }\n        }\n        return keepers.entrySet().stream().map(e -> e.getValue().key).toList();\n    }\n\n    public enum DeityField implements JsonNodeReader {\n        altNames,\n        category,\n        pantheon,\n        province,\n        symbol,\n        title,\n        reprintAlias\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteFeat.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.ArrayNode;\nimport com.fasterxml.jackson.databind.node.TextNode;\n\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.tools.JsonNodeReader;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.HomebrewIndex.HomebrewMetaTypes;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteFeat;\nimport dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase;\n\npublic class Json2QuteFeat extends Json2QuteCommon {\n    public Json2QuteFeat(Tools5eIndex index, Tools5eIndexType type, JsonNode jsonNode) {\n        super(index, type, jsonNode);\n    }\n\n    @Override\n    protected Tools5eQuteBase buildQuteResource() {\n        Tags tags = new Tags(getSources());\n        tags.add(\"feat\");\n\n        List<ImageRef> images = getFluffImages(Tools5eIndexType.featFluff);\n        Collection<HomebrewMetaTypes> metas = index.getHomebrewMetaTypes(sources);\n\n        String category = JsonSource.featureTypeToString(FeatFields.category.getTextOrEmpty(rootNode), Map.of());\n        if (category.startsWith(\"Fighting Style, \")) {\n            category = \"Fighting Style Replacement\";\n        }\n\n        // Initialize full entries with ability score increases merged\n        JsonNode fullEntries = initFullEntries();\n\n        // Convert entries to text\n        List<String> text = new ArrayList<>();\n        appendToText(text, fullEntries, null);\n\n        // TODO: update w/ additionalSpells\n        return new QuteFeat(sources,\n                linkifier().decoratedName(type, rootNode),\n                getSourceText(sources),\n                listPrerequisites(rootNode),\n                null, // Level coming someday..\n                SkillOrAbility.getAbilityScoreIncreases(FeatFields.ability.getFrom(rootNode)),\n                category,\n                images,\n                String.join(\"\\n\", text),\n                tags);\n    }\n\n    JsonNode initFullEntries() {\n        ArrayNode entries = SourceField.entries.readArrayFrom(rootNode);\n\n        var abilities = FeatFields.ability.streamFrom(rootNode)\n                .filter(x -> !Tools5eFields.hidden.booleanOrDefault(x, false))\n                .toList();\n\n        // If there are no abilities, just return the entries as is\n        if (abilities.isEmpty()) {\n            return entries;\n        }\n\n        // Find the first element of type \"list\"\n        var targetList = streamOf(entries)\n                .filter(node -> SourceField.type.getTextOrEmpty(node).equals(\"list\"))\n                .findFirst().orElse(null);\n\n        if (targetList != null) {\n            var items = Tools5eFields.items.readArrayFrom(targetList);\n            boolean allItems = Tools5eFields.items.streamFrom(targetList)\n                    .allMatch(x -> SourceField.type.getTextOrEmpty(x).equals(\"item\"));\n\n            for (JsonNode abilityNode : abilities) {\n                items.insert(0, allItems\n                        ? entryToListItemItem(abilityNode)\n                        : entryToListItemText(abilityNode));\n            }\n        } else {\n            // No list found, handle other cases...\n            int firstEntriesIndex = -1;\n            for (int i = 0; i < entries.size(); i++) {\n                if (SourceField.type.getTextOrEmpty(entries.get(i)).equals(\"entries\")) {\n                    firstEntriesIndex = i;\n                    break;\n                }\n            }\n            if (firstEntriesIndex >= 0) {\n                // Gather all displayed abilities\n                var abilityEntries = mapper().createArrayNode();\n                for (JsonNode abilityNode : abilities) {\n                    abilityEntries.add(entryToListItemText(abilityNode));\n                }\n\n                // Create new entries element for abilities\n                var abilityEntry = mapper().createObjectNode()\n                        .put(\"type\", \"entries\")\n                        .put(\"name\", \"Ability Score Increase\")\n                        .set(\"entries\", abilityEntries);\n\n                // Insert the new ability entry at the beginning\n                entries.insert(firstEntriesIndex, abilityEntry);\n            } else {\n                // No nested entries found, just return the original entries\n                for (JsonNode abilityNode : abilities) {\n                    entries.insert(0, entryToListItemText(abilityNode));\n                }\n            }\n        }\n\n        return entries;\n    }\n\n    JsonNode entryToListItemText(JsonNode abilityNode) {\n        return new TextNode(SkillOrAbility.getAbilityScoreIncrease(abilityNode));\n    }\n\n    JsonNode entryToListItemItem(JsonNode abilityNode) {\n        return mapper().createObjectNode()\n                .put(\"type\", \"item\")\n                .put(\"name\", \"Ability Score Increase\")\n                .put(\"entry\", SkillOrAbility.getAbilityScoreIncrease(abilityNode));\n    }\n\n    enum FeatFields implements JsonNodeReader {\n        ability,\n        additionalSpells,\n        category,\n        ;\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteHazard.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.tools.JsonNodeReader;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteHazard;\nimport dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase;\n\npublic class Json2QuteHazard extends Json2QuteCommon {\n\n    public Json2QuteHazard(Tools5eIndex index, Tools5eIndexType type, JsonNode jsonNode) {\n        super(index, type, jsonNode);\n    }\n\n    @Override\n    protected Tools5eQuteBase buildQuteResource() {\n        Tags tags = new Tags(getSources());\n\n        String hazardType = TrapHazFields.trapHazType.getTextOrNull(rootNode);\n        if (hazardType != null) {\n            tags.add(\"hazard\", hazardType);\n        }\n\n        List<ImageRef> images = new ArrayList<>();\n        List<String> text = getFluff(type == Tools5eIndexType.trap\n                ? Tools5eIndexType.trapFluff\n                : Tools5eIndexType.hazardFluff, \"##\", images);\n        appendToText(text, SourceField.entries.getFrom(rootNode), \"##\");\n\n        return new QuteHazard(getSources(),\n                getSources().getName(),\n                getSourceText(getSources()),\n                getHazardType(hazardType),\n                images,\n                String.join(\"\\n\", text),\n                tags);\n    }\n\n    enum TrapHazFields implements JsonNodeReader {\n        trapHazType\n    }\n\n    String getHazardType(String key) {\n        if (key == null) {\n            return \"Generic Hazard\";\n        }\n        return switch (key) {\n            case \"ENV\" -> \"Environmental Hazard\";\n            case \"EST\" -> \"Eldritch Storm\";\n            case \"MAG\" -> \"Magical Trap\";\n            case \"MECH\" -> \"Mechanical Trap\";\n            case \"WTH\" -> \"Weather\";\n            case \"WLD\" -> \"Wilderness Hazard\";\n            default -> \"Generic Hazard\"; // also \"GEN\"\n        };\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteItem.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static dev.ebullient.convert.StringUtil.isPresent;\nimport static dev.ebullient.convert.StringUtil.join;\nimport static dev.ebullient.convert.StringUtil.joinConjunct;\nimport static dev.ebullient.convert.StringUtil.uppercaseFirst;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.TreeSet;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.tools.JsonNodeReader;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteItem;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteItem.Variant;\nimport dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase;\n\npublic class Json2QuteItem extends Json2QuteCommon {\n    static final List<String> hiddenRarity = List.of(\"none\", \"unknown\", \"unknown (magic)\", \"varies\");\n\n    Json2QuteItem(Tools5eIndex index, Tools5eIndexType type, JsonNode jsonNode) {\n        super(index, type, jsonNode);\n    }\n\n    @Override\n    protected Tools5eQuteBase buildQuteResource() {\n\n        Tags tags = new Tags(getSources());\n        Variant rootVariant = createVariant(rootNode, tags);\n\n        List<Variant> variants = new ArrayList<>();\n        if (ItemField._variants.existsIn(rootNode)) {\n            for (JsonNode variantNode : iterableElements(ItemField._variants.getFrom(rootNode))) {\n                Variant variant = createVariant(variantNode, tags);\n                variants.add(variant);\n            }\n        }\n\n        List<ImageRef> fluffImages = new ArrayList<>();\n        String text = itemText(fluffImages);\n\n        if (type == Tools5eIndexType.itemGroup) {\n            List<String> itemLinks = ItemField.items.linkifyListFrom(rootNode, Tools5eIndexType.item, this);\n            text = (text == null ? \"\" : text + \"\\n\\n\")\n                    + \"**Items in this group:**\\n\\n- \" + String.join(\"\\n- \", itemLinks);\n        }\n\n        return new QuteItem(sources,\n                getSourceText(sources),\n                rootVariant,\n                variants,\n                fluffImages,\n                text,\n                tags);\n    }\n\n    private Variant createVariant(JsonNode variantNode, Tags tags) {\n        Tools5eSources variantSources = Tools5eSources.findOrTemporary(variantNode);\n        boolean pushed = parseState().push(variantNode);\n        try {\n            ItemType itemType = getItemType(variantSources, variantNode, ItemField.type);\n            ItemType itemTypeAlt = getItemType(variantSources, variantNode, ItemField.typeAlt);\n\n            Set<ItemProperty> itemProperties = new TreeSet<>(ItemProperty.comparator);\n            findProperties(variantSources, variantNode, itemProperties, itemType, itemTypeAlt);\n\n            Set<ItemMastery> itemMasteries = new TreeSet<>(ItemMastery.comparator);\n            findMastery(variantSources, variantNode, itemMasteries);\n\n            String damage = null;\n            String damage2h = null;\n            if (ItemField.dmgType.existsIn(variantNode)) {\n                String dmg1 = ItemField.dmg1.getTextOrEmpty(variantNode);\n                String dmg2 = ItemField.dmg2.getTextOrEmpty(variantNode);\n                String dmgType = damageTypeToFull(ItemField.dmgType.getTextOrEmpty(variantNode));\n                damage = dmg1 + \" \" + dmgType;\n                if (dmg2 != null && !dmg2.isBlank()) {\n                    damage2h = dmg2 + \" \" + dmgType;\n                }\n            }\n\n            String baseItemKey = ItemField.baseItem.getTextOrEmpty(variantNode);\n            String baseItem = linkify(Tools5eIndexType.item, baseItemKey);\n            boolean baseItemIncluded = false;\n\n            boolean ammo = ItemField.ammo.booleanOrDefault(variantNode, false);\n            boolean cursed = ItemField.curse.booleanOrDefault(variantNode, false);\n            boolean firearm = ItemField.firearm.booleanOrDefault(variantNode, false);\n            boolean poison = ItemField.poison.booleanOrDefault(variantNode, false);\n            boolean staff = ItemField.staff.booleanOrDefault(variantNode, false);\n            boolean tattoo = ItemField.tattoo.booleanOrDefault(variantNode, false);\n            boolean wondrous = ItemField.wondrous.booleanOrDefault(variantNode, false);\n\n            boolean focus = ItemField.focus.existsIn(variantNode)\n                    || ItemField.scfType.existsIn(variantNode);\n\n            String age = ItemField.age.getTextOrEmpty(variantNode);\n            String weaponCategory = ItemField.weaponCategory.getTextOrEmpty(variantNode);\n\n            String attunement = attunement(variantNode);\n            String rarity = ItemField.rarity.getTextOrEmpty(variantNode);\n            String tier = ItemField.tier.getTextOrEmpty(variantNode);\n\n            String poisonTypes = poison\n                    ? joinConjunct(\" or \", ItemField.poisonTypes.getListOfStrings(variantNode, tui()))\n                    : null;\n\n            // -- render.js -------------------------\n            // const [typeListText, typeHtml, subTypeHtml] =\n            // Renderer.item.getHtmlAndTextTypes(item);\n            // Building typeDescription and subtypeDescription in a stable order\n            List<String> typeDescription = new ArrayList<>();\n            List<String> subTypeDescription = new ArrayList<>();\n\n            if (wondrous) {\n                typeDescription.add(\"wondrous item\" + (tattoo ? \" (tattoo)\" : \"\"));\n                if (tattoo) {\n                    ItemTag.wondrous.add(tags, \"tattoo\");\n                }\n            }\n            if (staff) {\n                typeDescription.add(\"staff\");\n            }\n            if (ammo) {\n                typeDescription.add(\"ammunition\");\n            }\n            if (isPresent(age)) {\n                ItemTag.age.add(tags, age);\n                subTypeDescription.add(age);\n            }\n            if (isPresent(weaponCategory)) {\n                ItemTag.weapon.add(tags, weaponCategory);\n                baseItemIncluded = isPresent(baseItem);\n                typeDescription.add(\"weapon\"\n                        + (baseItemIncluded ? \" (\" + baseItem + \")\" : \"\"));\n                subTypeDescription.add(weaponCategory + \" weapon\");\n            }\n            if (staff && (EncodedType.M.typeIn(itemType, itemTypeAlt))) {\n                // \"M\" --> Type: Melee weapon\n                // DMG p140: \"Unless a staff's description says otherwise, a staff can be used\n                // as a quarterstaff.\"\n                subTypeDescription.add(\"melee weapon\");\n            }\n            if (itemType != null) {\n                tags.addRaw(ItemType.tagForType(itemType, tui()));\n                processType(itemType, typeDescription, subTypeDescription, baseItem, baseItemIncluded);\n                subTypeDescription.add(itemType.linkify());\n            }\n            if (itemTypeAlt != null) {\n                tags.addRaw(ItemType.tagForType(itemTypeAlt, tui()));\n                processType(itemTypeAlt, typeDescription, subTypeDescription, baseItem, baseItemIncluded);\n                subTypeDescription.add(itemTypeAlt.linkify());\n            }\n            if (firearm) {\n                subTypeDescription.add(\"firearm\");\n            }\n            if (poison) {\n                itemProperties.add(ItemProperty.POISON);\n                typeDescription.add(\"poison\" + (isPresent(poisonTypes) ? \" (\" + poisonTypes + \")\" : \"\"));\n            }\n            if (cursed) {\n                itemProperties.add(ItemProperty.CURSED);\n                typeDescription.add(\"cursed item\");\n            }\n\n            // Begin creation of detail string;\n            // render.js getAttunementAndAttunementCatText(item);\n            // getTypeRarityAndAttunementText(item);\n            // getTypeRarityAndAttunementHtml\n            String detail = join(\", \", typeDescription);\n            if (\"other\".equals(detail)) {\n                detail = \"\";\n            }\n\n            if (isPresent(tier)) {\n                ItemTag.tier.add(tags, tier);\n                detail += (detail.isBlank() ? \"\" : \", \") + tier;\n            }\n            if (isPresent(rarity)) {\n                ItemTag.rarity.add(tags, rarity\n                        .replace(\"very rare\", \"very-rare\")\n                        .replaceAll(\"[()]\", \"\") // unknown (magic) -> unknown magic\n                        .split(\" \"));\n                if (!hiddenRarity.contains(rarity)) {\n                    detail += (detail.isBlank() ? \"\" : \", \") + rarity;\n                }\n            }\n            if (isPresent(attunement)) {\n                ItemTag.attunement.add(tags,\n                        attunement.equals(\"optional\") ? \"optional\" : \"required\");\n\n                detail += (detail.isBlank() ? \"\" : \" \")\n                        + switch (attunement) {\n                            case \"required\" -> \"(requires attunement)\";\n                            case \"optional\" -> \"(attunement optional)\";\n                            default -> \"(requires attunement \" + attunement + \")\";\n                        };\n            }\n\n            return new Variant(\n                    itemName(variantNode),\n                    uppercaseFirst(detail),\n                    uppercaseFirst(join(\", \", subTypeDescription)),\n                    baseItem,\n                    itemType == null ? \"\" : itemType.name(),\n                    itemTypeAlt == null ? \"\" : itemTypeAlt.name(),\n                    ItemProperty.asLinks(itemProperties),\n                    ItemMastery.asLinks(itemMasteries),\n                    armorClass(variantNode, itemType, itemTypeAlt),\n                    weaponCategory,\n                    damage,\n                    damage2h,\n                    ItemField.range.getTextOrNull(variantNode),\n                    ItemField.strength.intOrNull(variantNode),\n                    ItemField.stealth.booleanOrDefault(variantNode, false),\n                    listPrerequisites(variantNode),\n                    age,\n                    coinValue(variantNode),\n                    ItemField.value.intOrNull(variantNode),\n                    ItemField.weight.doubleOrNull(variantNode),\n                    rarity,\n                    tier,\n                    attunement,\n                    ammo,\n                    firearm,\n                    cursed,\n                    focus,\n                    focus ? focusType(variantNode) : \"\",\n                    poison,\n                    poisonTypes,\n                    staff,\n                    tattoo,\n                    wondrous,\n                    ItemField.bonusAc.getTextOrNull(variantNode),\n                    ItemField.bonusWeapon.getTextOrNull(variantNode),\n                    ItemField.bonusWeaponAttack.getTextOrNull(variantNode),\n                    ItemField.bonusWeaponDamage.getTextOrNull(variantNode),\n                    ItemField.bonusWeaponCritDamage.getTextOrNull(variantNode),\n                    ItemField.bonusSpellAttack.getTextOrNull(variantNode),\n                    ItemField.bonusSpellDamage.getTextOrNull(variantNode),\n                    ItemField.bonusSpellSaveDc.getTextOrNull(variantNode),\n                    ItemField.bonusSavingThrow.getTextOrNull(variantNode),\n                    ItemField.bonusAbilityCheck.getTextOrNull(variantNode),\n                    ItemField.bonusProficiencyBonus.getTextOrNull(variantNode),\n                    ItemField.bonusSavingThrowConcentration.getTextOrNull(variantNode));\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    // render.js _getHtmlAndTextTypes_type\n    private void processType(ItemType type,\n            List<String> typeDescription, List<String> subTypeDescription,\n            String baseItem, boolean baseItemIncluded) {\n\n        String allTypes = typeDescription.toString();\n        String fullType = type.lowercaseName();\n\n        boolean isSubType = (type.group() == ItemTypeGroup.weapon && allTypes.contains(\"weapon\"))\n                || (type.group() == ItemTypeGroup.armor && allTypes.contains(\"armor\"));\n        List<String> target = isSubType ? subTypeDescription : typeDescription;\n\n        if (EncodedType.S.typeIn(type, null)) {\n            target.add(\"armor (\" + linkify(Tools5eIndexType.item, \"shield|phb\") + \")\");\n        } else if (!baseItemIncluded && isPresent(baseItem)) {\n            target.add(fullType + \" (\" + baseItem + \")\");\n        } else if (EncodedType.GV.not(type)) {\n            target.add(fullType);\n        }\n    }\n\n    private String attunement(JsonNode variantNode) {\n        // render.js -- getAttunementAndAttunementCatText\n        String attunement = ItemField.reqAttune.getTextOrEmpty(variantNode);\n        if (!isPresent(attunement)) {\n            attunement = ItemField.reqAttuneAlt.getTextOrEmpty(variantNode);\n        }\n        return switch (attunement) {\n            case \"\", \"false\" -> \"\";\n            case \"true\" -> \"required\";\n            case \"optional\" -> \"optional\";\n            default -> replaceText(attunement);\n        };\n    }\n\n    private String focusType(JsonNode variantNode) {\n        List<String> focusTypes = new ArrayList<>();\n        JsonNode focusNode = ItemField.focus.getFrom(variantNode);\n        if (focusNode != null && focusNode.isArray()) {\n            focusNode.forEach(x -> focusTypes.add(\n                    linkify(Tools5eIndexType.classtype, x.asText())));\n        }\n        String scfType = ItemField.scfType.getTextOrEmpty(variantNode);\n        return scfType\n                + (!isPresent(scfType) || focusTypes.isEmpty() ? \"\" : \"; \")\n                + join(\", \", focusTypes);\n    }\n\n    private String coinValue(JsonNode variantNode) {\n        Integer value = ItemField.value.intOrNull(variantNode);\n        return value == null ? null : convertCurrency(value);\n    }\n\n    String itemName(JsonNode variantNode) {\n        Tools5eSources vSources = Tools5eSources.findSources(variantNode);\n        if (Tools5eSources.isSrd(variantNode)) {\n            if (index().sourceIncluded(vSources.primarySource())) {\n                return vSources.getName();\n            }\n            String srdName = Tools5eSources.srdName(variantNode);\n            if (srdName != null) {\n                return srdName;\n            }\n        }\n        return vSources.getName();\n    }\n\n    String itemText(List<ImageRef> imageRef) {\n        List<String> text = getFluff(Tools5eIndexType.itemFluff, \"##\", imageRef);\n\n        if (rootNode.has(\"entries\")) {\n            maybeAddBlankLine(text);\n            for (JsonNode entry : iterableEntries(rootNode)) {\n                if (entry.isTextual()) {\n                    String input = entry.asText();\n                    if (input.startsWith(\"{#itemEntry \")) {\n                        insertItemRefText(text, input);\n                    } else {\n                        maybeAddBlankLine(text);\n                        text.add(replaceText(input));\n                    }\n                } else {\n                    appendToText(text, entry, \"##\");\n                }\n            }\n        }\n\n        return text.isEmpty() ? null : String.join(\"\\n\", text);\n    }\n\n    void insertItemRefText(List<String> text, String input) {\n        String finalKey = Tools5eIndexType.itemEntry.fromTagReference(input.replaceAll(\"\\\\{#itemEntry (.*)}\", \"$1\"));\n        if (finalKey == null || index.isExcluded(finalKey)) {\n            return;\n        }\n        JsonNode ref = index.getNode(finalKey);\n        if (ref == null) {\n            tui().errorf(\"Could not find %s from %s\", finalKey, getSources());\n        } else if (index.sourceIncluded(ref.get(\"source\").asText())) {\n            try {\n                String entriesTemplate = mapper().writeValueAsString(ref.get(\"entriesTemplate\"));\n                if (ItemField.detail1.existsIn(rootNode)) {\n                    entriesTemplate = entriesTemplate.replaceAll(\"\\\\{\\\\{item.detail1}}\",\n                            ItemField.detail1.getTextOrEmpty(rootNode));\n                }\n                if (ItemField.resist.existsIn(rootNode)) {\n                    entriesTemplate = entriesTemplate.replaceAll(\"\\\\{\\\\{(getFullImmRes\\\\s)?item.resist}}\",\n                            joinAndReplace(ItemField.resist.readArrayFrom(rootNode)));\n                }\n                appendToText(text, mapper().readTree(entriesTemplate), \"##\");\n            } catch (JsonProcessingException e) {\n                tui().errorf(e, \"Unable to insert item element text for %s from %s\", input, getSources());\n            }\n        }\n    }\n\n    String armorClass(JsonNode variantNode, ItemType type, ItemType typeAlt) {\n        String ac = ItemField.ac.getTextOrEmpty(variantNode);\n        if (!isPresent(ac)) {\n            return null;\n        }\n        // - If you wear light armor, you add your Dexterity modifier to the base number\n        // from your armor type to determine your Armor Class.\n        // - If you wear medium armor, you add your Dexterity modifier, to a maximum of\n        // +2,\n        // to the base number from your armor type to determine your Armor Class.\n        // - Heavy armor does not let you add your Dexterity modifier to your Armor\n        // Class,\n        // but it also does not penalize you if your Dexterity modifier is negative.\n        if (EncodedType.LA.typeIn(type, typeAlt)) {\n            ac += \" + Dex modifier\";\n        } else if (EncodedType.MA.typeIn(type, typeAlt)) {\n            ac += \" + Dex modifier (max of +2)\";\n        }\n        return ac;\n    }\n\n    ItemType getItemType(Tools5eSources variantSources, JsonNode node, ItemField typeField) {\n        String abbv = typeField.getTextOrEmpty(node);\n        return index.findItemType(abbv, variantSources);\n    }\n\n    void findProperties(Tools5eSources variantSources, JsonNode variantNode,\n            Set<ItemProperty> itemProperties, ItemType type, ItemType typeAlt) {\n\n        JsonNode propertyList = ItemField.property.getFrom(variantNode);\n        if (propertyList != null && propertyList.isArray()) {\n            // List of properties: abbreviation, or abbreviation|source\n            for (JsonNode x : iterableElements(propertyList)) {\n                ItemProperty p = index.findItemProperty(x.asText(), variantSources);\n                if (p != null) {\n                    itemProperties.add(p);\n                }\n            }\n        }\n\n        String lowerName = SourceField.name.getTextOrEmpty(variantNode).toLowerCase();\n        if ((ItemTypeGroup.weapon.hasGroup(type, typeAlt)) && lowerName.contains(\"silvered\")) {\n            // Add property to link to section on silvered weapons\n            itemProperties.add(ItemProperty.SILVERED);\n        }\n    }\n\n    void findMastery(Tools5eSources variantSources, JsonNode variantNode,\n            Set<ItemMastery> itemMasteries) {\n        JsonNode masteryList = ItemField.mastery.getFrom(variantNode);\n        if (masteryList != null && masteryList.isArray()) {\n            for (JsonNode x : iterableElements(masteryList)) {\n                ItemMastery mastery = index.findItemMastery(x.asText(), variantSources);\n                if (mastery != null) {\n                    itemMasteries.add(mastery);\n                }\n            }\n        }\n    }\n\n    enum EncodedType {\n        GV, // generic variant\n        LA, // light armor\n        MA, // medium armor\n        M, // melee weapon\n        S, // shield\n        ;\n\n        boolean typeIn(ItemType type, ItemType typeAlt) {\n            return (type != null && name().equalsIgnoreCase(type.abbreviation()))\n                    || (typeAlt != null && name().equalsIgnoreCase(typeAlt.abbreviation()));\n        }\n\n        boolean not(ItemType type) {\n            return type == null || !name().equalsIgnoreCase(type.abbreviation());\n        }\n    }\n\n    enum ItemField implements JsonNodeReader {\n        _variants,\n        ac,\n        age,\n        ammo,\n        attunement,\n        baseItem,\n        bonusAc,\n        bonusWeapon,\n        bonusWeaponAttack,\n        bonusWeaponDamage,\n        bonusWeaponCritDamage,\n        bonusSpellAttack,\n        bonusSpellDamage,\n        bonusSpellSaveDc,\n        bonusSavingThrow,\n        bonusAbilityCheck,\n        bonusProficiencyBonus,\n        bonusSavingThrowConcentration,\n        curse,\n        detail1,\n        dmg1,\n        dmg2,\n        dmgType,\n        firearm,\n        focus,\n        hasFluff,\n        hasFluffImages,\n        items,\n        mastery,\n        packContents,\n        poison,\n        poisonTypes,\n        property,\n        range,\n        rarity,\n        reqAttune,\n        reqAttuneAlt,\n        resist,\n        scfType,\n        sentient,\n        staff,\n        stealth,\n        strength,\n        tattoo,\n        tier,\n        type,\n        typeAlt,\n        value,\n        weaponCategory,\n        weight,\n        wondrous,\n        ;\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteLegendaryGroup.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.tools.JsonNodeReader;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteNote;\n\npublic class Json2QuteLegendaryGroup extends Json2QuteCommon {\n\n    Json2QuteLegendaryGroup(Tools5eIndex index, Tools5eIndexType type, JsonNode jsonNode) {\n        super(index, type, jsonNode);\n    }\n\n    @Override\n    protected Tools5eQuteNote buildQuteNote() {\n        Tags tags = new Tags(getSources());\n        tags.add(\"monster\", \"legendary-group\");\n\n        List<String> text = new ArrayList<>();\n        appendSectionText(text, LeGroupFields.lairActions, \"Lair Actions\");\n        appendSectionText(text, LeGroupFields.regionalEffects, \"Regional Effects\");\n        appendSectionText(text, LeGroupFields.mythicEncounter, \"As a Mythic Encounter\");\n\n        return new Tools5eQuteNote(sources,\n                sources.getName(),\n                null,\n                String.join(\"\\n\", text),\n                tags)\n                .withTargetFile(linkifier().getTargetFileName(getName(), sources))\n                .withTargetPath(linkifier().getRelativePath(type));\n    }\n\n    void appendSectionText(List<String> text, LeGroupFields field, String header) {\n        JsonNode node = field.getFrom(rootNode);\n        if (node == null || node.isMissingNode() || node.isNull()) {\n            return;\n        }\n        boolean pushed = parseState().push(node);\n        try {\n            maybeAddBlankLine(text);\n            text.add(\"## \" + header);\n            text.add(getSourceText(parseState()));\n            text.add(\"\");\n            appendToText(text, node, \"###\");\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    enum LeGroupFields implements JsonNodeReader {\n        lairActions,\n        mythicEncounter,\n        regionalEffects,\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteMonster.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static dev.ebullient.convert.StringUtil.pluralize;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.TreeMap;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.ArrayNode;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport com.fasterxml.jackson.databind.node.TextNode;\n\nimport dev.ebullient.convert.io.Msg;\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.qute.NamedText;\nimport dev.ebullient.convert.tools.JsonNodeReader;\nimport dev.ebullient.convert.tools.JsonSourceCopier.MetaFields;\nimport dev.ebullient.convert.tools.JsonTextConverter;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.ToolsIndex.TtrpgValue;\nimport dev.ebullient.convert.tools.dnd5e.qute.AbilityScores;\nimport dev.ebullient.convert.tools.dnd5e.qute.AcHp;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteMonster;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteMonster.HiddenType;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteMonster.Initiative;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteMonster.InitiativeMode;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteMonster.SavesAndSkills;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteMonster.SavingThrow;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteMonster.SkillModifier;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteMonster.Spellcasting;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteMonster.Spells;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteMonster.TraitDescription;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteMonster.Traits;\nimport dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase;\n\npublic class Json2QuteMonster extends Json2QuteCommon {\n    public static boolean isNpc(JsonNode source) {\n        return MonsterFields.isNpc.booleanOrDefault(source,\n                MonsterFields.isNamedCreature.booleanOrDefault(source,\n                        false));\n    }\n\n    String creatureType;\n    String subtype;\n    AcHp acHp = new AcHp();\n    final boolean isNpc;\n\n    Json2QuteMonster(Tools5eIndex index, Tools5eIndexType type, JsonNode jsonNode) {\n        super(index, type, jsonNode);\n        findCreatureType();\n        findAc(acHp);\n        findHp(acHp);\n        isNpc = isNpc(rootNode);\n    }\n\n    @Override\n    protected Tools5eQuteBase buildQuteResource() {\n        String size = getSize(rootNode);\n        String environment = joinAndReplace(rootNode, \"environment\");\n        String cr = monsterCr(rootNode);\n        String pb = monsterPb(cr);\n\n        Tags tags = new Tags(getSources());\n        tags.add(\"monster\", \"size\", size);\n        tags.add(\"monster\", \"cr\", cr);\n        if (subtype == null || subtype.isEmpty()) {\n            tags.add(\"monster\", \"type\", creatureType);\n        } else {\n            for (String detail : subtype.split(\"\\\\s*,\\\\s*\")) {\n                tags.add(\"monster\", \"type\", creatureType, detail);\n            }\n        }\n        if (!environment.isBlank()) {\n            for (String env : environment.split(\"\\\\s*,\\\\s*\")) {\n                tags.add(\"monster\", \"environment\", env);\n            }\n        }\n\n        List<ImageRef> fluffImages = new ArrayList<>();\n        String fluff = getFluffDescription(Tools5eIndexType.monsterFluff, \"##\", fluffImages);\n\n        AbilityScores abilityScores = abilityScores(rootNode);\n\n        return new QuteMonster(sources,\n                linkifier().decoratedName(type, rootNode),\n                getSourceText(sources),\n                isNpc,\n                size, creatureType, subtype, monsterAlignment(),\n                acHp,\n                speed(Tools5eFields.speed.getFrom(rootNode)),\n                abilityScores,\n                monsterSavesAndSkills(),\n                linkedSenses(),\n                intOrDefault(rootNode, \"passive\", 10),\n                immuneResist(),\n                gear(),\n                joinAndReplace(rootNode, \"languages\"),\n                cr, pb,\n                initiative(abilityScores, cr),\n                collectAllTraits(),\n                monsterSpellcasting(),\n                fluff,\n                environment,\n                getToken(),\n                fluffImages,\n                tags);\n    }\n\n    void findCreatureType() {\n        JsonNode typeNode = type == Tools5eIndexType.monster\n                ? SourceField.type.getFrom(rootNode)\n                : MonsterFields.creatureType.getFrom(rootNode);\n        if (typeNode == null) {\n            if (type == Tools5eIndexType.monster) {\n                tui().warnf(\"Empty type for %s\", getSources());\n            }\n            return;\n        }\n        if (typeNode.isTextual()) {\n            creatureType = mapType(typeNode.asText());\n            return;\n        }\n\n        // We have an object: type + tags\n        creatureType = mapType(SourceField.type.getTextOrEmpty(typeNode));\n        List<String> tags = new ArrayList<>();\n        typeNode.withArray(\"tags\").forEach(tag -> {\n            if (tag.isTextual()) {\n                tags.add(tag.asText());\n            } else {\n                tags.add(String.format(\"%s %s\",\n                        tag.get(\"prefix\").asText(),\n                        tag.get(\"tag\").asText()));\n            }\n        });\n        if (!tags.isEmpty()) {\n            subtype = String.join(\", \", tags);\n        }\n    }\n\n    // Aliases for consistency. Hard-coded for common types/corrections. Won't be fool-proof\n    String mapType(String type) {\n        for (MonsterType t : MonsterType.values()) {\n            if (type.toLowerCase().startsWith(t.name())) {\n                return t.name();\n            }\n        }\n        switch (type.toLowerCase()) {\n            case \"abberation\", \"abberations\" -> {\n                return MonsterType.aberration.name();\n            }\n            case \"creature\", \"creatures\" -> {\n                if (getName().toLowerCase().contains(\"zoblin\")) {\n                    return MonsterType.undead.name();\n                }\n                if (getName().toLowerCase().contains(\"fandom\")) {\n                    return MonsterType.humanoid.name();\n                }\n                return MonsterType.beast.name();\n            }\n            case \"golem\", \"golems\" -> {\n                subtype = \"golem\";\n                return MonsterType.construct.name();\n            }\n            default -> {\n                return type;\n            }\n        }\n    }\n\n    String monsterPb(String cr) {\n        if (cr != null) {\n            return \"+\" + crToPb(cr);\n        }\n        return \"+2\";\n    }\n\n    SavesAndSkills monsterSavesAndSkills() {\n        SavesAndSkills savesSkills = new SavesAndSkills();\n\n        JsonNode saveNode = MonsterFields.save.getFrom(rootNode);\n        if (saveNode != null) {\n            savesSkills.saves = new ArrayList<>();\n            for (Entry<String, JsonNode> f : iterableFields(saveNode)) {\n                savesSkills.saves.add(getSavingThrow(f.getKey(), f.getValue()));\n            }\n        }\n\n        JsonNode skillNode = MonsterFields.skill.getFrom(rootNode);\n        if (skillNode != null) {\n            savesSkills.skills = new ArrayList<>();\n            for (var skill : iterableFields(skillNode)) {\n                String name = skill.getKey();\n                JsonNode value = skill.getValue();\n                if (\"other\".equalsIgnoreCase(name)) {\n                    savesSkills.skillChoices = new ArrayList<>();\n                    for (var item : ensureArray(value)) {\n                        JsonNode oneOf = MonsterFields.oneOf.getFrom(item);\n                        if (oneOf == null) {\n                            tui().errorf(\"What is this (from %s): %s\", sources.getKey(), item);\n                            continue;\n                        }\n                        List<SkillModifier> choices = new ArrayList<>();\n                        for (var e : iterableFields(oneOf)) {\n                            choices.add(getModifier(e.getKey(), e.getValue()));\n                        }\n                        savesSkills.skillChoices.add(choices);\n                    }\n                } else {\n                    savesSkills.skills.add(getModifier(name, value));\n                }\n            }\n        }\n        return savesSkills;\n    }\n\n    SavingThrow getSavingThrow(String name, JsonNode node) {\n        SkillOrAbility save = SkillOrAbility.fromTextValue(name);\n        name = save == null ? name : save.value();\n        String text = node.asText();\n        if (text.matches(\"[+-]?\\\\d+\")) {\n            return new SavingThrow(name, node.asInt());\n        } else {\n            return new SavingThrow(name, replaceText(text));\n        }\n    }\n\n    SkillModifier getModifier(String name, JsonNode value) {\n        SkillOrAbility skill = SkillOrAbility.fromTextValue(name);\n        name = skill == null ? name : skill.value();\n        String link = skill == null ? null : linkifySkill(skill);\n        String text = value.asText();\n        if (text.matches(\"[+-]?\\\\d+\")) {\n            return new SkillModifier(name, link, value.asInt());\n        } else {\n            return new SkillModifier(name, link, replaceText(text));\n        }\n    }\n\n    // _getInitiativeBonus\n    Initiative initiative(AbilityScores abilityScores, String cr) {\n        JsonNode initiative = MonsterFields.initiative.getFrom(rootNode);\n        if (initiative == null) {\n            // if (mon.initiative == null && (mon.dex == null || mon.dex.special)) return null;\n            if (abilityScores.dexterity() == null || abilityScores.dexterity().isSpecial()) {\n                return null;\n            }\n            // if (mon.initiative == null) return Parser.getAbilityModNumber(mon.dex);\n            return new Initiative(abilityScores.dexterity().modifier());\n        }\n        // if (typeof mon.initiative === \"number\") return mon.initiative;\n        if (initiative.isNumber()) {\n            return new Initiative(initiative.asInt());\n        }\n        // if (typeof mon.initiative !== \"object\") return null;\n        if (!initiative.isObject()) {\n            return null;\n        }\n        // if (typeof mon.initiative.initiative === \"number\") return mon.initiative.initiative;\n        JsonNode value = MonsterFields.initiative.getFrom(initiative);\n        if (value != null && value.isNumber()) {\n            return new Initiative(value.asInt());\n        }\n        // if (mon.dex == null) return;\n        if (abilityScores.dexterity() == null) {\n            return null;\n        }\n        InitiativeMode advantageMode = InitiativeMode.fromString(\n                MonsterFields.advantageMode.getTextOrNull(initiative));\n        // 1 is proficient, 2 is expert\n        int profBonus = 0;\n        int profLevel = MonsterFields.proficiency.intOrDefault(initiative, 0);\n        double crValue = crToNumber(cr);\n        if (profLevel > 0 && crValue < CR_CUSTOM) {\n            profBonus = profLevel * crToPb(cr);\n        }\n        return new Initiative(\n                abilityScores.dexterity().modifier() + profBonus,\n                advantageMode);\n    }\n\n    String monsterAlignment() {\n        ArrayNode a1 = rootNode.withArray(\"alignment\");\n        if (a1.size() == 0) {\n            return \"Unaligned\";\n        }\n        if (a1.size() == 1 && a1.get(0).has(\"special\")) {\n            return a1.get(0).get(\"special\").asText();\n        }\n\n        String prefix = MonsterFields.alignmentPrefix.getTextOrDefault(rootNode, \"\");\n        prefix = (prefix.isEmpty() ? \"\" : prefix + \" \");\n\n        String choices = a1.toString();\n        if (choices.contains(\"note\")) {\n            List<String> notes = new ArrayList<>(List.of(choices.split(\"},\\\\{\")));\n            for (int i = 0; i < notes.size(); i++) {\n                int pos = notes.get(i).indexOf(\"note\");\n                String alignment = mapAlignmentToString(toAlignmentCharacters(notes.get(i).substring(0, pos)));\n                String note = notes.get(i).substring(pos + 4).replaceAll(\"[^A-Za-z ]+\", \"\");\n                notes.set(i, String.format(\"%s (%s)\", alignment, note));\n            }\n            return prefix + String.join(\", \", notes);\n        } else {\n            choices = toAlignmentCharacters(choices);\n            return prefix + mapAlignmentToString(choices);\n        }\n    }\n\n    List<Spellcasting> monsterSpellcasting() {\n        boolean pushed = parseState().pushTrait();\n        try {\n            ArrayNode array = MonsterFields.spellcasting.readArrayFrom(rootNode);\n            if (array == null || array.isNull()) {\n                return null;\n            } else if (array.isObject()) {\n                tui().warnf(Msg.UNKNOWN, \"Unknown spellcasting for %s: %s\", sources.getKey(), array.toPrettyString());\n                return null;\n            }\n\n            List<Spellcasting> casting = new ArrayList<>();\n            for (JsonNode scNode : iterableElements(array)) {\n                if (scNode == null || scNode.isNull()) {\n                    continue;\n                }\n                Spellcasting spellcasting = new Spellcasting();\n                spellcasting.name = SourceField.name.replaceTextFrom(scNode, this);\n                spellcasting.displayAs = MonsterFields.displayAs.getTextOrDefault(scNode, \"trait\");\n                spellcasting.hidden = MonsterFields.hidden.getListOfStrings(scNode, tui());\n                spellcasting.ability = MonsterFields.ability.getTextOrDefault(scNode, \"spellcasting\");\n\n                spellcasting.headerEntries = new ArrayList<>();\n                appendToText(spellcasting.headerEntries,\n                        MonsterFields.headerEntries.getFrom(scNode), null);\n\n                spellcasting.footerEntries = new ArrayList<>();\n                appendToText(spellcasting.footerEntries,\n                        MonsterFields.footerEntries.getFrom(scNode), null);\n\n                spellcasting.fixed = new HashMap<>();\n                spellcasting.variable = new HashMap<>();\n\n                for (var type : HiddenType.values()) {\n                    JsonNode value = scNode.get(type.name());\n                    if (value == null || value.isNull()) {\n                        continue;\n                    }\n                    switch (type) {\n                        case constant, will, ritual -> {\n                            List<String> spellList = getSpells(value);\n                            if (spellList.isEmpty()) {\n                                continue;\n                            }\n                            spellcasting.fixed.put(type.name(), spellList);\n                        }\n                        case spells -> {\n                            spellcasting.spells = new TreeMap<>();\n                            for (Entry<String, JsonNode> f : iterableFields(MonsterFields.spells.getFrom(scNode))) {\n                                // value is object defining slots and spells\n                                JsonNode spellNode = f.getValue();\n\n                                Spells spellContainer = new Spells();\n                                spellContainer.spells = getSpells(MonsterFields.spells.getFrom(spellNode));\n                                if (spellContainer.spells.isEmpty()) {\n                                    continue;\n                                }\n                                spellContainer.slots = MonsterFields.slots.intOrDefault(spellNode, 0);\n                                spellContainer.lowerBound = MonsterFields.lower.intOrDefault(spellNode, 0);\n\n                                // key is level\n                                spellcasting.spells.put(f.getKey(), spellContainer);\n                            }\n                        }\n                        default -> {\n                            Map<String, List<String>> frequencySpells = new HashMap<>();\n                            for (Entry<String, JsonNode> freqSpellList : iterableFields(value)) {\n                                String frequency = freqSpellList.getKey();\n                                List<String> spellList = getSpells(freqSpellList.getValue());\n                                if (spellList.isEmpty()) {\n                                    continue;\n                                }\n                                frequencySpells.put(frequency, spellList);\n                            }\n                            if (frequencySpells.isEmpty()) {\n                                continue;\n                            }\n                            spellcasting.variable.put(type.name(), frequencySpells);\n                        }\n                    }\n                }\n                parseState().popCitations(spellcasting.footerEntries);\n                casting.add(spellcasting);\n            }\n            return casting;\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    List<String> getSpells(JsonNode source) {\n        if (source == null || source.isNull()) {\n            tui().errorf(\"Null spells from %s\", sources.getKey());\n            return List.of();\n        }\n        List<String> spells = new ArrayList<>();\n        for (var item : iterableElements(source)) {\n            if (item.isTextual()) {\n                spells.add(toLink(item.asText()));\n            } else if (item.isObject()) {\n                boolean hidden = MonsterFields.hidden.booleanOrDefault(item, false);\n                if (hidden) {\n                    continue;\n                }\n                spells.add(toLink(SourceField.entry.getTextOrEmpty(item)));\n            } else {\n                tui().warnf(Msg.UNKNOWN, \"Unknown spell type for %s: %s\", sources.getKey(), item.toPrettyString());\n            }\n        }\n        return spells;\n    }\n\n    private String toLink(String spellText) {\n        return spellText.contains(\"{@\")\n                ? replaceText(spellText)\n                : linkify(Tools5eIndexType.spell, spellText);\n    }\n\n    Traits collectAllTraits() {\n        boolean pushed = parseState().pushTrait();\n        try {\n            String legendaryGroupLink = null;\n            TraitDescription lairActions = null;\n            TraitDescription regionalEffects = null;\n\n            JsonNode lgNameSource = MonsterFields.legendaryGroup.getFrom(rootNode);\n            String lgKey = index().getAliasOrDefault(Tools5eIndexType.legendaryGroup.createKey(lgNameSource));\n            if (lgNameSource != null && index().isIncluded(lgKey)) {\n                JsonNode lgNode = index.getOrigin(lgKey);\n                Tools5eSources lgSources = Tools5eSources.findSources(lgKey);\n                lairActions = traitDescription(lgNode, MonsterFields.lairActions, \"Lair Actions\");\n                regionalEffects = traitDescription(lgNode, MonsterFields.regionalEffects, \"Regional Effects\");\n                legendaryGroupLink = linkifyLegendaryGroup(lgSources);\n            }\n\n            return new Traits(\n                    traitDescription(rootNode, MonsterFields.trait, \"Traits\"),\n                    traitDescription(rootNode, MonsterFields.action, \"Actions\"),\n                    traitDescription(rootNode, MonsterFields.bonus, \"Bonus Actions\"),\n                    traitDescription(rootNode, MonsterFields.reaction, \"Reactions\"),\n                    traitDescription(rootNode, MonsterFields.legendary, \"Legendary Actions\"),\n                    lairActions,\n                    regionalEffects,\n                    traitDescription(rootNode, MonsterFields.mythic, \"Mythic Actions\"),\n                    legendaryGroupLink);\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    TraitDescription traitDescription(JsonNode source, MonsterFields field, String title) {\n        boolean pushed = parseState().pushTrait();\n        try {\n            String noteKey = field.name() + \"Note\";\n            JsonNode noteNode = source.get(noteKey);\n            if (noteNode != null && noteNode.isTextual()) {\n                title += \" <small>(\" + replaceText(noteNode) + \")</small>\";\n            }\n\n            String headerKey = field.name() + \"Header\";\n            JsonNode headerNode = source.get(headerKey);\n            String headerText = null;\n            if (headerNode != null) {\n                headerText = flattenToString(headerNode);\n            }\n\n            List<NamedText> traits = new ArrayList<>();\n            JsonNode target = field.getFrom(source);\n            if (target != null && target.isArray()) {\n                collectTraits(traits, target);\n            }\n\n            if (traits.size() > 0 && field == MonsterFields.legendary && headerText == null) {\n                int legendaryActionCount = MonsterFields.legendaryActions.intOrDefault(rootNode, 3);\n                int legendaryActionsLairCount = MonsterFields.legendaryActionsLair.intOrDefault(rootNode, legendaryActionCount);\n                boolean isNamedCreature = MonsterFields.isNamedCreature.booleanOrDefault(rootNode, false);\n                var possessive = isNamedCreature ? \"their\" : \"its\";\n\n                if (getSources().isClassic()) {\n                    var shortName = Tools5eJsonSourceCopier.getShortName(rootNode, true);\n                    // The dragon can take 3 legendary actions, choosing from the options below.\n                    // Only one legendary action can be used at a time and only at the end of another creature's turn.\n                    // The dragon regains spent legendary actions at the start of its turn.\n                    headerText = replaceText(\n                            \"%s can take %d legendary action%s%s, choosing from the options below. Only one legendary action can be used at a time and only at the end of another creature's turn. %s regains spent legendary actions at the start of %s turn.\"\n                                    .formatted(shortName,\n                                            legendaryActionCount,\n                                            legendaryActionCount == 1 ? \"\" : \"s\",\n                                            legendaryActionsLairCount != legendaryActionCount\n                                                    ? \" (or %d when in %s lair)\".formatted(legendaryActionsLairCount,\n                                                            possessive)\n                                                    : \"\",\n                                            shortName,\n                                            possessive));\n                } else {\n                    // Legendary Action Uses: 3 (4 in Lair).\n                    // Immediately after another creature's turn, The dragon can expend a use to take one of the following actions.\n                    // The dragon regains all expended uses at the start of each of its turns.\n                    headerText = replaceText(\n                            \"Legendary Action Uses: %d%s. Immediately after another creature's turn, %s can expend a use to take one of the following actions. %s regains all expended uses at the start of each of %s turns.\"\n                                    .formatted(\n                                            legendaryActionCount,\n                                            legendaryActionsLairCount != legendaryActionCount\n                                                    ? \" (%d in Lair)\".formatted(legendaryActionsLairCount)\n                                                    : \"\",\n                                            Tools5eJsonSourceCopier.getShortName(rootNode, false),\n                                            Tools5eJsonSourceCopier.getShortName(rootNode, true),\n                                            possessive));\n                }\n            }\n            return new TraitDescription(title, headerText, traits);\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    String linkedSenses() {\n        JsonNode node = MonsterFields.senses.getFrom(rootNode);\n        if (node == null || node.isNull()) {\n            return \"\";\n        }\n        if (node.isTextual()) {\n            return linkifySense(node.asText());\n        }\n        List<String> list = new ArrayList<>();\n        for (JsonNode senseNode : iterableElements(node)) {\n            list.add(linkifySense(senseNode.asText()));\n        }\n        return String.join(\", \", list);\n    }\n\n    String linkifySense(String sense) {\n        int pos = sense.indexOf(\" \"); // find first space\n        if (pos < 0) {\n            return linkify(Tools5eIndexType.sense, sense);\n        }\n        return replaceText(\"{@sense %s}%s\".formatted(sense.substring(0, pos), sense.substring(pos)));\n    }\n\n    List<String> gear() {\n        final List<MonsterFields> gearFields = List.of(\n                MonsterFields.gear, MonsterFields.attachedItems);\n        List<String> gear = new ArrayList<>();\n        for (MonsterFields field : gearFields) {\n            for (var node : field.iterateArrayFrom(rootNode)) {\n                if (node == null || node.isNull() || node.isArray()) {\n                    continue;\n                }\n                int quantity = MonsterFields.quantity.intOrDefault(node, 1);\n                String item = node.isObject()\n                        ? MonsterFields.item.getTextOrEmpty(node)\n                        : node.asText();\n                if (quantity == 1) {\n                    gear.add(linkify(Tools5eIndexType.item, item));\n                } else {\n                    var name = item.split(\"\\\\|\")[0];\n                    var plural = pluralize(name, quantity);\n                    gear.add(replaceText(\"%s {@item %s|%s}\".formatted(numberToText(quantity), item, plural)));\n                }\n            }\n        }\n        return gear;\n    }\n\n    @Override\n    public String getImagePath() {\n        if (type != Tools5eIndexType.monster) {\n            return super.getImagePath();\n        }\n        return linkifier().monsterPath(isNpc, creatureType);\n    }\n\n    public static List<JsonNode> findMonsterVariants(\n            Tools5eIndex index, Tools5eIndexType type,\n            String key, JsonNode jsonSource) {\n\n        if (key.contains(\"splugoth the returned\") || key.contains(\"prophetess dran\")) {\n            MonsterFields.isNpc.setIn(jsonSource, true); // Fix.\n        }\n\n        for (JsonNode variant : MonsterFields.variant.iterateArrayFrom(jsonSource)) {\n            // There is a code path that is only followed if a variant also has _version\n            // but it doesn't seem like there are any examples of this in the data.\n            if (MonsterFields._versions.existsIn(variant)) {\n                Tui.instance().warnf(Msg.SOMEDAY, \"\\\"Variant for %s has versions: %s\", key, variant);\n            }\n        }\n\n        boolean summonedCreature = MonsterFields.summonedBySpellLevel.existsIn(jsonSource);\n        boolean hasVersions = MonsterFields._versions.existsIn(jsonSource);\n\n        if (summonedCreature || hasVersions) {\n            List<JsonNode> versions = new ArrayList<>();\n            List<String> versionKeys = new ArrayList<>();\n            boolean replacedPrimary = false;\n            final String origname = SourceField.name.getTextOrEmpty(jsonSource);\n\n            // Expand versions first\n            if (hasVersions) {\n                for (JsonNode vNode : MonsterFields._versions.iterateArrayFrom(jsonSource)) {\n                    if (MonsterFields._abstract.existsIn(vNode) && MonsterFields._implementations.existsIn(vNode)) {\n                        versions.addAll(getVersionsTemplate(vNode));\n                    } else {\n                        versions.add(getVersionsBasic(vNode));\n                    }\n                }\n\n                // With each version...\n                for (JsonNode vNode : versions) {\n                    // DataUtil.generic._getVersion(...)\n                    String vKey = hydrateVersion(key, jsonSource, (ObjectNode) vNode, index);\n                    versionKeys.add(vKey);\n                    String variantName = SourceField.name.getTextOrEmpty(vNode);\n                    if (variantName.equals(origname)) {\n                        replacedPrimary = true;\n                    }\n                }\n                TtrpgValue.indexVersionKeys.setIn(jsonSource, Tui.MAPPER.valueToTree(versionKeys));\n            }\n\n            // Add original after processing versions\n            if (!replacedPrimary) {\n                versions.add(0, jsonSource);\n            }\n            return versions;\n        }\n        return List.of(jsonSource);\n    }\n\n    public static String hydrateVersion(String parentKey, JsonNode parentSource, ObjectNode version, Tools5eIndex index) {\n        // DataUtil.generic._hydrateVersion({key}, {source}, {version})\n\n        Tools5eIndexType type = Tools5eIndexType.monster;\n        String versionKey = type.createKey(version);\n\n        ObjectNode parentCopy = (ObjectNode) parentSource.deepCopy();\n        MonsterFields._versions.removeFrom(parentCopy);\n        Tools5eFields.hasToken.removeFrom(parentCopy);\n        Tools5eFields.hasFluff.removeFrom(parentCopy);\n        Tools5eFields.hasFluffImages.removeFrom(parentCopy);\n\n        filterSources(Tools5eFields.additionalSources, parentCopy, SourceField.source.getTextOrNull(version));\n        filterSources(Tools5eFields.otherSources, parentCopy, SourceField.source.getTextOrNull(version));\n\n        index.copier.mergeNodes(type, parentKey, parentCopy, version);\n        TtrpgValue.indexParentKey.setIn(version, parentKey);\n\n        Tools5eSources.constructSources(versionKey, version);\n        return versionKey;\n    }\n\n    private static void filterSources(JsonNodeReader field, ObjectNode parentCopy, String vesionSource) {\n        if (vesionSource == null) {\n            return;\n        }\n        JsonNode sources = field.ensureArrayIn(parentCopy);\n        Iterator<JsonNode> it = sources.elements();\n        while (it.hasNext()) {\n            JsonNode source = it.next();\n            if (vesionSource.equals(source.asText())) {\n                it.remove();\n            }\n        }\n        if (sources.isEmpty()) {\n            field.removeFrom(parentCopy);\n        }\n    }\n\n    public static JsonNode getVersionsBasic(JsonNode version) {\n        mutExpandCopy(version);\n        return version;\n    }\n\n    public static List<JsonNode> getVersionsTemplate(JsonNode version) {\n        // DataUtil.generic._getVersions_template({ver})\n        return MonsterFields._implementations.streamFrom(version)\n                .map(impl -> {\n                    JsonNode cpyTemplate = MonsterFields._abstract.copyFrom(version);\n                    mutExpandCopy(cpyTemplate);\n\n                    ObjectNode cpyImpl = impl.deepCopy();\n                    JsonNode _variables = MonsterFields._variables.removeFrom(cpyImpl);\n                    if (_variables != null) {\n                        cpyTemplate = replaceTemplateVariables(cpyTemplate, _variables);\n                    }\n                    ((ObjectNode) cpyTemplate).setAll(cpyImpl);\n                    return cpyTemplate;\n                })\n                .toList();\n    }\n\n    /**\n     * Walk a JSON tree, replacing all {{varName}} patterns in text nodes\n     * with the corresponding value from the variables object.\n     * Mirrors 5etools: MiscUtil.getWalker().walk(cpyTemplate, {string: str => str.replace(...)})\n     */\n    static JsonNode replaceTemplateVariables(JsonNode node, JsonNode variables) {\n        if (node.isTextual()) {\n            String text = node.asText();\n            if (text.contains(\"{{\")) {\n                Matcher matcher = Pattern.compile(\"\\\\{\\\\{([^}]+)}}\").matcher(text);\n                String replaced = matcher.replaceAll(m -> {\n                    JsonNode value = variables.get(m.group(1));\n                    return value != null ? Matcher.quoteReplacement(value.asText()) : Matcher.quoteReplacement(m.group());\n                });\n                return new TextNode(replaced);\n            }\n            return node;\n        }\n        if (node.isObject()) {\n            ObjectNode obj = (ObjectNode) node;\n            var i = obj.fieldNames();\n            while (i.hasNext()) {\n                var field = i.next();\n                JsonNode replaced = replaceTemplateVariables(obj.get(field), variables);\n                if (replaced != obj.get(field)) {\n                    obj.set(field, replaced);\n                }\n            }\n            return obj;\n        }\n        if (node.isArray()) {\n            ArrayNode arr = (ArrayNode) node;\n            for (int i = 0; i < arr.size(); i++) {\n                JsonNode replaced = replaceTemplateVariables(arr.get(i), variables);\n                if (replaced != arr.get(i)) {\n                    arr.set(i, replaced);\n                }\n            }\n            return arr;\n        }\n        return node;\n    }\n\n    public static void mutExpandCopy(JsonNode node) {\n        JsonNode _copy = Tui.MAPPER.createObjectNode();\n\n        // Move fields from the original node to the copy node\n        MetaFields._mod.moveFrom(node, _copy);\n\n        // Make sure a preserve element exists (which it will not if the original node is empty)\n        MetaFields._preserve.moveFrom(node, _copy);\n        if (!MetaFields._preserve.existsIn(_copy)) {\n            MetaFields._preserve.setIn(_copy, Tui.MAPPER.createObjectNode().put(\"*\", true));\n        }\n\n        // Copy the copy node back to the original node\n        MetaFields._copy.setIn(node, _copy);\n    }\n\n    public enum MonsterType {\n        aberration,\n        beast,\n        celestial,\n        construct,\n        dragon,\n        elemental,\n        fey,\n        fiend,\n        giant,\n        humanoid,\n        monstrosity,\n        ooze,\n        plant,\n        undead,\n        miscellaneous;\n\n        public String toDirectory() {\n            return name();\n        }\n\n        public static MonsterType fromString(String type) {\n            String compare = type.toLowerCase()\n                    .replace(\"abberation\", \"aberration\"); // correct typo\n            for (MonsterType t : MonsterType.values()) {\n                if (compare.startsWith(t.name().toLowerCase())) {\n                    return t;\n                }\n            }\n            return miscellaneous;\n        }\n\n        public static MonsterType fromNode(JsonNode node, JsonTextConverter<?> converter) {\n            if (!MonsterFields.type.existsIn(node)) {\n                Tools5eSources sources = Tools5eSources.findSources(node);\n                Tui.instance().warnf(\"Monster: Empty type for %s\", sources);\n                return miscellaneous;\n            }\n            JsonNode typeNode = MonsterFields.type.getFrom(node);\n            String text = null;\n            if (typeNode.isTextual()) {\n                text = converter.replaceText(typeNode.asText());\n            } else if (typeNode.isObject() && MonsterFields.type.existsIn(typeNode)) {\n                // We have an object: type + tags\n                text = MonsterFields.type.replaceTextFrom(typeNode, converter);\n            }\n            return text == null ? miscellaneous : fromString(text);\n        }\n\n        public static String toDirectory(String type) {\n            MonsterType t = fromString(type);\n            return t.toDirectory();\n        }\n    }\n\n    enum MonsterFields implements JsonNodeReader {\n        _abstract,\n        _implementations,\n        _variables,\n        _versions,\n        ability,\n        ac,\n        action,\n        actionHeader,\n        actionNote,\n        advantageMode,\n        alignment,\n        alignmentPrefix,\n        attachedItems,\n        average,\n        bonus,\n        bonusHeader,\n        bonusNote,\n        choose,\n        cr,\n        creatureType, // object -- alternate to monster type\n        daily,\n        displayAs,\n        footerEntries,\n        formula,\n        from,\n        gear,\n        headerEntries,\n        hidden,\n        hp,\n        initiative,\n        isNamedCreature,\n        isNpc,\n        item,\n        lairActions,\n        legendary,\n        legendaryActions,\n        legendaryActionsLair,\n        legendaryGroup,\n        legendaryHeader,\n        lower,\n        mythic,\n        mythicHeader,\n        oneOf,\n        original,\n        proficiency,\n        quantity,\n        reaction,\n        reactionHeader,\n        reactionNote,\n        regionalEffects,\n        save,\n        senses,\n        skill,\n        slots,\n        special,\n        spellcasting,\n        spells,\n        summonedBySpellLevel,\n        trait,\n        type,\n        variant,\n        will,\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteNote.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteNote;\n\npublic class Json2QuteNote extends Json2QuteCommon {\n\n    final String title;\n    boolean useSuffix;\n\n    Json2QuteNote(Tools5eIndex index, Tools5eIndexType type, JsonNode jsonNode) {\n        super(index, type, jsonNode);\n        title = index.replaceText(jsonNode.get(\"name\").asText());\n    }\n\n    Json2QuteNote useSuffix(boolean useSuffix) {\n        this.useSuffix = useSuffix;\n        return this;\n    }\n\n    @Override\n    public String getName() {\n        return title;\n    }\n\n    @Override\n    protected Tools5eQuteNote buildQuteNote() {\n        Tags tags = new Tags(getSources());\n        String targetFile = useSuffix\n                ? linkifier().getTargetFileName(getName(), getSources())\n                : null;\n\n        return new Tools5eQuteNote(title,\n                getSourceText(sources),\n                getText(\"##\"),\n                tags)\n                .withTargetFile(targetFile);\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteObject.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.tools.JsonNodeReader;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteObject;\nimport dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase;\n\npublic class Json2QuteObject extends Json2QuteMonster {\n\n    Json2QuteObject(Tools5eIndex index, Tools5eIndexType type, JsonNode jsonNode) {\n        super(index, type, jsonNode);\n    }\n\n    @Override\n    protected Tools5eQuteBase buildQuteResource() {\n        String size = getSize(rootNode);\n        String objectType = findObjectType();\n\n        Tags tags = new Tags(getSources());\n        tags.add(\"object\", \"size\", size);\n        tags.add(\"object\", \"type\", objectType);\n        if (creatureType != null) {\n            tags.add(\"object\", \"type\", creatureType);\n        }\n\n        List<ImageRef> fluffImages = new ArrayList<>();\n        List<String> text = getFluff(Tools5eIndexType.objectFluff, \"##\", fluffImages);\n        appendToText(text, SourceField.entries.getFrom(rootNode), \"##\");\n\n        return new QuteObject(sources,\n                getSources().getName(),\n                getSourceText(sources),\n                isNpc, size,\n                creatureType, objectType,\n                acHp,\n                speed(Tools5eFields.speed.getFrom(rootNode)),\n                abilityScores(rootNode),\n                joinAndReplace(rootNode, \"senses\"),\n                immuneResist(),\n                collectTraits(\"actionEntries\"),\n                getToken(), fluffImages,\n                String.join(\"\\n\", text),\n                tags);\n    }\n\n    private String findObjectType() {\n        String type = ObjectFields.objectType.getTextOrEmpty(rootNode);\n        return switch (type) {\n            case \"G\", \"GEN\" -> \"Generic\";\n            case \"SW\" -> \"Siege weapon\";\n            case \"U\" -> \"Unknown\";\n            default -> type;\n        };\n    }\n\n    enum ObjectFields implements JsonNodeReader {\n        objectType\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteOptionalFeature.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteFeat;\nimport dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase;\n\npublic class Json2QuteOptionalFeature extends Json2QuteCommon {\n    public Json2QuteOptionalFeature(Tools5eIndex index, Tools5eIndexType type, JsonNode jsonNode) {\n        super(index, type, jsonNode);\n    }\n\n    @Override\n    protected Tools5eQuteBase buildQuteResource() {\n        Tags tags = new Tags(getSources());\n        List<String> typeList = Tools5eFields.featureType.getListOfStrings(rootNode, tui());\n        for (String featureType : typeList) {\n            tags.add(\"optional-feature\", featureType);\n        }\n\n        String featureTypeFull = index.getOptionalFeatureType(typeList.get(0)).getTitle();\n        if (featureTypeFull.startsWith(\"Fighting Style\")) {\n            featureTypeFull = \"Fighting Style\"; //trim class name, fighting styles can be for multiple classes\n        } else if (featureTypeFull.equalsIgnoreCase(\"Maneuver, Battle Master\")) {\n            featureTypeFull = \"Battle Master Maneuver\";\n        } else if (featureTypeFull.equalsIgnoreCase(\"Maneuver, Cavalier V2 (UA)\")) {\n            featureTypeFull = \"Cavalier Maneuver, V2 (UA)\";\n        }\n\n        List<ImageRef> images = new ArrayList<>();\n        List<String> text = getFluff(Tools5eIndexType.optionalfeatureFluff, \"##\", images);\n        appendToText(text, SourceField.entries.getFrom(rootNode), \"##\");\n\n        return new QuteFeat(getSources(),\n                getSources().getName(),\n                getSourceText(sources),\n                listPrerequisites(rootNode),\n                null,\n                null,\n                featureTypeFull,\n                images,\n                String.join(\"\\n\", text),\n                tags);\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteOptionalFeatureType.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.List;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.OptionalFeatureIndex.OptionalFeatureType;\nimport dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteNote;\n\npublic class Json2QuteOptionalFeatureType extends Json2QuteCommon {\n\n    final OptionalFeatureType oft;\n    final String title;\n\n    Json2QuteOptionalFeatureType(Tools5eIndex index, JsonNode node, OptionalFeatureType optionalFeatureType) {\n        super(index, Tools5eIndexType.optionalFeatureTypes, node);\n        this.oft = optionalFeatureType;\n        this.title = optionalFeatureType.getTitle();\n    }\n\n    @Override\n    public String getName() {\n        return title;\n    }\n\n    @Override\n    protected Tools5eQuteNote buildQuteNote() {\n        List<String> featureKeys = oft.features;\n        List<JsonNode> nodes = featureKeys.stream()\n                .map(index::getAliasOrDefault)\n                .distinct()\n                .map(index::getNode)\n                .filter(x -> x != null)\n                .sorted(Comparator.comparing(SourceField.name::getTextOrEmpty))\n                .toList();\n\n        String key = super.sources.getKey();\n        if (nodes.isEmpty() || index().isExcluded(key)) {\n            return null;\n        }\n\n        Tags tags = new Tags(getSources());\n        List<String> text = new ArrayList<>();\n\n        for (JsonNode entry : nodes) {\n            Tools5eSources sources = Tools5eSources.findSources(entry);\n            text.add(\"- \" + linkifier().link(sources));\n        }\n        if (text.isEmpty()) {\n            return null;\n        }\n\n        String sourceText = super.sources.getSourceText();\n        return new Tools5eQuteNote(title, sourceText, text, tags)\n                .withTargetFile(oft.getFilename())\n                .withTargetPath(linkifier().getRelativePath(Tools5eIndexType.optionalFeatureTypes));\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QutePsionicTalent.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.qute.NamedText;\nimport dev.ebullient.convert.tools.JsonNodeReader;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.HomebrewIndex.HomebrewMetaTypes;\nimport dev.ebullient.convert.tools.dnd5e.PsionicType.PsionicTypeEnum;\nimport dev.ebullient.convert.tools.dnd5e.qute.QutePsionic;\nimport dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase;\n\npublic class Json2QutePsionicTalent extends Json2QuteCommon {\n\n    Json2QutePsionicTalent(Tools5eIndex index, Tools5eIndexType type, JsonNode jsonNode) {\n        super(index, type, jsonNode);\n    }\n\n    @Override\n    protected Tools5eQuteBase buildQuteResource() {\n        Tags tags = new Tags(getSources());\n\n        List<String> text = new ArrayList<>();\n        appendToText(text, rootNode, \"##\");\n\n        return new QutePsionic(sources,\n                getName(),\n                getSourceText(sources),\n                getPsionicTypeOrder(),\n                PsionicFields.focus.replaceTextFrom(rootNode, this),\n                getPsionicModes(),\n                String.join(\"\\n\", text),\n                tags);\n    }\n\n    String getPsionicTypeOrder() {\n        String order = PsionicFields.order.replaceTextFrom(rootNode, this);\n        Collection<HomebrewMetaTypes> metas = index.getHomebrewMetaTypes(sources);\n\n        String typeName = PsionicFields.type.getTextOrDefault(rootNode, \"\\u2014\");\n        PsionicType type = switch (typeName) {\n            case \"D\" -> PsionicTypeEnum.Discipline;\n            case \"T\" -> PsionicTypeEnum.Talent;\n            default -> {\n                for (var meta : metas) {\n                    var t = meta.getPsionicType(typeName);\n                    if (t != null) {\n                        yield t;\n                    }\n                }\n                yield null;\n            }\n        };\n\n        return type == null ? order : type.combineWith(order);\n    }\n\n    Collection<NamedText> getPsionicModes() {\n        List<NamedText> traits = new ArrayList<>();\n        for (JsonNode e : PsionicFields.modes.iterateArrayFrom(rootNode)) {\n            String name = getModeName(e);\n            List<String> text = collectEntries(e);\n\n            if (PsionicFields.submodes.existsIn(e)) {\n                maybeAddBlankLine(text);\n                for (JsonNode submode : PsionicFields.submodes.iterateArrayFrom(e)) {\n                    String submodeName = getModeName(submode);\n                    List<String> submodeText = collectEntries(submode);\n                    prependField(submodeName, submodeText);\n\n                    if (submodeText.size() > 0) {\n                        text.add(\"- \" + submodeText.get(0) + \"  \");\n                        submodeText.remove(0);\n                        submodeText.forEach(x -> text.add(x.isEmpty() ? \"\" : \"    \" + x + \"  \"));\n                    }\n                }\n            }\n\n            traits.add(new NamedText(name, String.join(\"\\n\", text)));\n        }\n        return traits;\n    }\n\n    String getModeName(JsonNode mode) {\n        String name = SourceField.name.replaceTextFrom(mode, this);\n        List<String> amendWith = new ArrayList<>();\n        if (PsionicFields.cost.existsIn(mode)) {\n            JsonNode cost = PsionicFields.cost.getFrom(mode);\n            int max = PsionicFields.max.intOrThrow(cost);\n            int min = PsionicFields.min.intOrThrow(cost);\n            if (max == min) {\n                amendWith.add(max + \" psi\");\n            } else {\n                amendWith.add(min + \"-\" + max + \" psi\");\n            }\n        }\n        if (PsionicFields.concentration.existsIn(mode)) {\n            JsonNode concentration = PsionicFields.concentration.getFrom(mode);\n            amendWith.add(String.format(\"conc., %s %s\",\n                    PsionicFields.duration.intOrThrow(concentration),\n                    PsionicFields.unit.getTextOrThrow(concentration)));\n        }\n\n        return amendWith.isEmpty() ? name\n                : (name + \" (\" + String.join(\"; \", amendWith) + \")\");\n    }\n\n    enum PsionicFields implements JsonNodeReader {\n        concentration,\n        cost,\n        duration,\n        focus,\n        max,\n        min,\n        modes,\n        order,\n        submodes,\n        type,\n        unit\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteRace.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static dev.ebullient.convert.StringUtil.isPresent;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.ArrayNode;\nimport com.fasterxml.jackson.databind.node.BooleanNode;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\n\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.tools.JsonNodeReader;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.ToolsIndex.TtrpgValue;\nimport dev.ebullient.convert.tools.dnd5e.Json2QuteFeat.FeatFields;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteRace;\n\npublic class Json2QuteRace extends Json2QuteCommon {\n\n    final static Pattern subraceNamePattern = Pattern.compile(\"^(.*?)\\s*\\\\((.*?)\\\\)$\");\n\n    Json2QuteRace(Tools5eIndex index, Tools5eIndexType type, JsonNode jsonNode) {\n        super(index, type, jsonNode);\n    }\n\n    @Override\n    protected QuteRace buildQuteResource() {\n        if (RaceFields._rawName.existsIn(rootNode)) {\n            tui().debugf(\"Skipping output of base race %s\", sources.getKey());\n            return null;\n        }\n        String name = linkifier().decoratedName(type, rootNode);\n        Tags tags = new Tags(getSources());\n\n        String[] split = name.split(\"\\\\(\");\n        for (int i = 0; i < split.length; i++) {\n            split[i] = slugify(split[i].trim());\n        }\n        String tagRoot = cfg().racesAsSpecies() ? \"species\" : \"race\";\n        tags.addRaw(tagRoot, String.join(\"/\", split));\n\n        List<ImageRef> fluffImages = new ArrayList<>();\n        String fluff = getFluffDescription(Tools5eIndexType.raceFluff, \"###\", fluffImages);\n\n        return new QuteRace(sources,\n                name,\n                getSourceText(sources),\n                getRaceAbility(),\n                creatureTypes(),\n                getSize(rootNode),\n                getSpeed(rootNode),\n                spellcastingAbility(),\n                getText(\"###\"),\n                fluff,\n                fluffImages,\n                tags);\n    }\n\n    String getSpeed(JsonNode value) {\n        JsonNode speed = value.get(\"speed\");\n        try {\n            if (speed == null) {\n                return \"30 ft.\";\n            } else if (speed.isTextual()) {\n                return speed.asText();\n            } else if (speed.isIntegralNumber()) {\n                return speed.asText() + \" ft.\";\n            } else if (speed.isObject()) {\n                List<String> list = new ArrayList<>();\n                for (var f : speed.properties()) {\n                    if (f.getValue().isIntegralNumber()) {\n                        list.add(String.format(\"%s: %s ft.\",\n                                f.getKey(), f.getValue().asText()));\n                    } else if (f.getValue().isBoolean()) {\n                        list.add(f.getKey() + \" equal to your walking speed\");\n                    }\n                }\n                return String.join(\"; \", list);\n            }\n        } catch (IllegalArgumentException ignored) {\n        }\n        tui().errorf(\"Unable to parse speed for %s from %s\", getSources(), speed);\n        return \"30 ft.\";\n    }\n\n    String creatureTypes() {\n        List<String> types = new ArrayList<>();\n        for (JsonNode x : iterableElements(rootNode.get(\"creatureTypes\"))) {\n            types.add(x.asText());\n        }\n        return types.isEmpty()\n                ? null\n                : String.join(\", \", types);\n    }\n\n    String spellcastingAbility() {\n        if (rootNode.has(\"additionalSpells\") && rootNode.get(\"additionalSpells\").isArray()) {\n            JsonNode spells = rootNode.get(\"additionalSpells\").get(0);\n            if (spells.has(\"ability\")) {\n                JsonNode ability = spells.get(\"ability\");\n                if (ability.has(\"choose\")) {\n                    List<String> abilities = new ArrayList<>();\n                    ability.withArray(\"choose\")\n                            .forEach(x -> abilities.add(SkillOrAbility.format(x.asText(), index(), getSources())));\n                    return \"Choose one of \" + String.join(\", \", abilities);\n                } else {\n                    return asAbilityEnum(ability);\n                }\n            }\n        }\n        return null;\n    }\n\n    String getRaceAbility() {\n        // TODO: render.js / handleAbilitiesChoose\n        String lineage = RaceFields.lineage.getTextOrEmpty(rootNode);\n        if (lineage.equals(\"VRGR\")) {\n            // Custom Lineage:\n            return \"Choose one of: (a) Choose any +2, choose any other +1; (b) Choose any +1, choose any other +1, choose any other +1\";\n        } else if (lineage.equals(\"UA1\")) {\n            // Custom Lineage:\n            return \"Choose any +2, choose any other +1\";\n        }\n\n        String ability = SkillOrAbility.getAbilityScore(FeatFields.ability.getFrom(rootNode));\n        return isPresent(ability)\n                ? ability\n                : \"None\";\n    }\n\n    String decoratedAmount(int amount) {\n        if (amount >= 0) {\n            return \"+\" + amount;\n        }\n        return amount + \"\";\n    }\n\n    public static String getSubraceName(String raceName, String subraceName) {\n        if (subraceName == null) {\n            return raceName;\n        }\n        Matcher m = subraceNamePattern.matcher(raceName);\n        if (m.matches()) {\n            raceName = m.group(1);\n            subraceName = String.join(\"; \", List.of(m.group(2), subraceName));\n        }\n        return String.format(\"%s (%s)\", raceName, subraceName);\n    }\n\n    // render.js: mergeSubraces\n    public static void prepareBaseRace(Tools5eIndex tools5eIndex, JsonNode jsonSource, Set<JsonNode> subraces) {\n\n        // fix size\n        JsonNode size = Tools5eFields.size.getFrom(jsonSource);\n        if (size != null && size.isTextual()) {\n            Tools5eFields.size.removeFrom(jsonSource);\n            Tools5eFields.size.ensureArrayIn(jsonSource).add(size.asText());\n        }\n\n        // fix lineage: handled at the moment by getRaceAbility()\n        JsonNode lineageNode = RaceFields.lineage.getFrom(jsonSource);\n        if (lineageNode != null && lineageNode.isTextual()) {\n            String lineage = RaceFields.lineage.getTextOrThrow(jsonSource);\n\n            if (!RaceFields.ability.existsIn(jsonSource)) {\n                ArrayNode ability = RaceFields.ability.ensureArrayIn(jsonSource);\n                ArrayNode abilities = ability.arrayNode();\n                SkillOrAbility.allSkills.forEach(x -> abilities.add(x));\n\n                ObjectNode choice = ability.objectNode();\n                ObjectNode weighted = choice.objectNode();\n                choice.set(\"choose\", weighted);\n                weighted.set(\"from\", abilities);\n                weighted.set(\"weights\", weighted.arrayNode().add(2).add(1));\n\n                if (lineage.equalsIgnoreCase(\"VRGR\")) {\n                    ability.add(tools5eIndex.copyNode(choice));\n                    weighted.set(\"weights\", weighted.arrayNode().add(1).add(1).add(1));\n                    ability.add(choice);\n                } else if (lineage.equalsIgnoreCase(\"UA1\")) {\n                    ability.add(choice);\n                }\n            }\n\n            ArrayNode entries = SourceField.entries.ensureArrayIn(jsonSource);\n            entries.add(entries.objectNode()\n                    .put(\"type\", \"entries\")\n                    .put(\"name\", \"Languages\")\n                    .put(\"entries\",\n                            \"You can speak, read, and write Common and one other language that you and your DM agree is appropriate for your character.\"));\n\n            if (!RaceFields.languageProficiencies.existsIn(jsonSource)) {\n                ArrayNode languageProficiencies = RaceFields.languageProficiencies.ensureArrayIn(jsonSource);\n                languageProficiencies.add(languageProficiencies.objectNode()\n                        .put(\"common\", true)\n                        .put(\"anyStandard\", 1));\n            }\n        }\n    }\n\n    public static void updateBaseRace(Tools5eIndex tools5eIndex, JsonNode jsonSource, Set<JsonNode> inputSubraces,\n            List<JsonNode> subraces) {\n\n        if (!RaceFields._isBaseRace.existsIn(jsonSource)) {\n            // If one of the original subraces was missing a name, it shares\n            // the base race name. Update the base race name to differentiate\n            // it from the subrace.\n            if (inputSubraces.stream().anyMatch(x -> !SourceField.name.existsIn(x))) {\n                String name = SourceField.name.getTextOrThrow(jsonSource);\n                RaceFields._rawName.setIn(jsonSource, name);\n                SourceField.name.setIn(jsonSource, name + \" (Base)\");\n                tools5eIndex.addAlias(\n                        Tools5eIndexType.race.createKey(jsonSource),\n                        TtrpgValue.indexKey.getTextOrThrow(jsonSource));\n            }\n\n            // subraces.sort((a, b) -> {\n            //     String aName = SourceField.name.getTextOrThrow(a);\n            //     String bName = SourceField.name.getTextOrThrow(b);\n            //     return aName.compareTo(bName);\n            // });\n\n            // ArrayNode entries = SourceField.entries.readArrayFrom(jsonSource);\n\n            // ArrayNode subraceList = entries.arrayNode();\n            // subraces.forEach(x -> subraceList.add(String.format(\"{@race %s|%s|%s (%s)}\",\n            //         SourceField.name.getTextOrThrow(x),\n            //         SourceField.source.getTextOrThrow(x),\n            //         SourceField.name.getTextOrThrow(x),\n            //         SourceField.source.getTextOrThrow(x))));\n\n            // ArrayNode sections = entries.arrayNode()\n            //         .add(entries.objectNode()\n            //                 .put(\"type\", \"section\")\n            //                 .set(\"entries\", entries.arrayNode()\n            //                         .add(\"This race has multiple subraces, as listed below:\")\n            //                         .add(entries.objectNode()\n            //                                 .put(\"type\", \"list\")\n            //                                 .set(\"items\", subraceList))))\n            //         .add(entries.objectNode()\n            //                 .put(\"type\", \"section\")\n            //                 .set(\"entries\", entries.objectNode()\n            //                         .put(\"type\", \"entries\")\n            //                         .put(\"name\", \"Traits\")\n            //                         .set(\"entries\", entries)));\n\n            // SourceField.entries.setIn(jsonSource, sections);\n            RaceFields._isBaseRace.setIn(jsonSource, BooleanNode.TRUE);\n        }\n    }\n\n    enum RaceFields implements JsonNodeReader {\n        _isBaseRace,\n        _isSubRace,\n        _rawName,\n        ability,\n        additionalSpells,\n        creatureTypes,\n        languageProficiencies,\n        skillProficiencies,\n        lineage,\n        race,\n        speed,\n        raceName,\n        raceSource\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteReward.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.tools.JsonNodeReader;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteReward;\n\npublic class Json2QuteReward extends Json2QuteCommon {\n\n    public Json2QuteReward(Tools5eIndex index, Tools5eIndexType type, JsonNode jsonNode) {\n        super(index, type, jsonNode);\n    }\n\n    @Override\n    protected QuteReward buildQuteResource() {\n        Tags tags = new Tags(getSources());\n\n        for (String type : SourceField.type.getListOfStrings(rootNode, tui())) {\n            tags.add(\"reward\", type);\n        }\n\n        List<ImageRef> images = new ArrayList<>();\n        List<String> text = getFluff(Tools5eIndexType.rewardFluff, \"##\", images);\n        appendToText(text, SourceField.entries.getFrom(rootNode), \"##\");\n\n        List<String> details = new ArrayList<>();\n        String type = RewardField.type.getTextOrNull(rootNode);\n        if (type != null) {\n            details.add(type);\n        }\n        String rarity = RewardField.rarity.getTextOrNull(rootNode);\n        if (rarity != null) {\n            details.add(rarity);\n        }\n        String detail = String.join(\", \", details);\n\n        return new QuteReward(getSources(),\n                getSources().getName(),\n                getSourceText(sources),\n                RewardField.ability.transformTextFrom(rootNode, \"\\n\", index),\n                getSources().getName().startsWith(detail) ? \"\" : detail,\n                RewardField.signaturespells.transformTextFrom(rootNode, \"\\n\", index),\n                images,\n                String.join(\"\\n\", text),\n                tags);\n    }\n\n    enum RewardField implements JsonNodeReader {\n        ability,\n        rarity,\n        signaturespells,\n        type\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteSpell.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static dev.ebullient.convert.StringUtil.join;\nimport static dev.ebullient.convert.StringUtil.pluralize;\nimport static dev.ebullient.convert.StringUtil.toOrdinal;\nimport static dev.ebullient.convert.StringUtil.uppercaseFirst;\n\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.TreeSet;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.tools.JsonNodeReader;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.SpellEntry.SpellReference;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteSpell;\nimport dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase;\n\npublic class Json2QuteSpell extends Json2QuteCommon {\n    final String decoratedName;\n\n    private static final Map<String, String> AREA_TAG_LABELS = Map.ofEntries(\n            Map.entry(\"C\", \"Cube\"),\n            Map.entry(\"H\", \"Hemisphere\"),\n            Map.entry(\"L\", \"Line\"),\n            Map.entry(\"MT\", \"Multiple Targets\"),\n            Map.entry(\"N\", \"Cone\"),\n            Map.entry(\"Q\", \"Square\"),\n            Map.entry(\"R\", \"Circle\"),\n            Map.entry(\"S\", \"Sphere\"),\n            Map.entry(\"ST\", \"Single Target\"),\n            Map.entry(\"W\", \"Wall\"),\n            Map.entry(\"Y\", \"Cylinder\"));\n\n    private static final Map<String, String> MISC_TAG_LABELS = Map.ofEntries(\n            Map.entry(\"AAD\", \"Additional Attack Damage\"),\n            Map.entry(\"ADV\", \"Grants Advantage\"),\n            Map.entry(\"DFT\", \"Difficult Terrain\"),\n            Map.entry(\"FMV\", \"Forced Movement\"),\n            Map.entry(\"HL\", \"Healing\"),\n            Map.entry(\"LGT\", \"Creates Light\"),\n            Map.entry(\"LGTS\", \"Creates Sunlight\"),\n            Map.entry(\"MAC\", \"Modifies AC\"),\n            Map.entry(\"OBJ\", \"Affects Objects\"),\n            Map.entry(\"OBS\", \"Obscures Vision\"),\n            Map.entry(\"PRM\", \"Permanent Effects\"),\n            Map.entry(\"PIR\", \"Permanent If Repeated\"),\n            Map.entry(\"PS\", \"Plane Shifting\"),\n            Map.entry(\"RO\", \"Rollable Effects\"),\n            Map.entry(\"SCL\", \"Scaling Effects\"),\n            Map.entry(\"SCT\", \"Scaling Targets\"),\n            Map.entry(\"SGT\", \"Requires Sight\"),\n            Map.entry(\"SMN\", \"Summons Creature\"),\n            Map.entry(\"THP\", \"Grants Temporary Hit Points\"),\n            Map.entry(\"TP\", \"Teleportation\"),\n            Map.entry(\"UBA\", \"Uses Bonus Action\"));\n\n    Json2QuteSpell(Tools5eIndex index, Tools5eIndexType type, JsonNode jsonNode) {\n        super(index, type, jsonNode);\n        decoratedName = linkifier().decoratedName(type, jsonNode);\n    }\n\n    @Override\n    protected Tools5eQuteBase buildQuteResource() {\n        SpellEntry spellEntry = index().getSpellIndex().getSpellEntry(getSources().getKey());\n\n        Tags tags = new Tags(getSources());\n\n        tags.add(\"spell\", \"school\", spellEntry.school.name());\n        tags.add(\"spell\", \"level\", JsonSource.spellLevelToText(spellEntry.level));\n        if (spellEntry.ritual) {\n            tags.add(\"spell\", \"ritual\");\n        }\n\n        // 🔧 Spell: spell|fireball|phb,\n        //    references: {subclass|destruction domain|cleric|phb|vss=subclass|destruction domain|cleric|phb|vss;c:5;s:null;null, ...}\n        //    expanded: {subclass|the fiend|warlock|phb|phb=subclass|the fiend|warlock|phb|phb;c:null;s:3;null, ...}\n        Set<String> referenceLinks = new HashSet<>();\n        Set<SpellReference> allRefs = new TreeSet<>(Comparator.comparing(x -> x.refererKey));\n        allRefs.addAll(spellEntry.references.values());\n        allRefs.addAll(spellEntry.expandedList.values());\n\n        for (var r : allRefs) {\n            tags.addRaw(r.tagifyReference());\n            referenceLinks.add(r.linkifyReference());\n        }\n\n        List<String> text = new ArrayList<>();\n        appendToText(text, rootNode, \"##\");\n        if (SpellFields.entriesHigherLevel.existsIn(rootNode)) {\n            maybeAddBlankLine(text);\n            appendToText(text, SpellFields.entriesHigherLevel.getFrom(rootNode),\n                    textContains(text, \"## \") ? \"##\" : null);\n        }\n        return new QuteSpell(sources,\n                decoratedName,\n                getSourceText(sources),\n                JsonSource.spellLevelToText(spellEntry.level),\n                spellEntry.school.name(),\n                spellEntry.ritual,\n                spellCastingTime(),\n                spellRange(),\n                spellComponents(),\n                spellDuration(),\n                spellAbilityChecks(),\n                spellAffectsCreatureTypes(),\n                spellAreaTags(),\n                spellConditionImmune(),\n                spellConditionInflict(),\n                spellDamageImmune(),\n                spellDamageInflict(),\n                spellDamageResist(),\n                spellDamageVulnerable(),\n                spellMiscTags(),\n                spellSavingThrows(),\n                spellScalingLevelDice(),\n                spellAttacks(),\n                spellHigherLevelEntries(),\n                referenceLinks,\n                getFluffImages(Tools5eIndexType.spellFluff),\n                String.join(\"\\n\", text),\n                tags);\n    }\n\n    SpellSchool getSchool() {\n        String code = SpellFields.school.getTextOrEmpty(rootNode);\n        return index().findSpellSchool(code, getSources());\n    }\n\n    boolean spellIsRitual() {\n        boolean ritual = false;\n        JsonNode meta = SpellFields.meta.getFrom(rootNode);\n        if (meta != null) {\n            ritual = SpellFields.ritual.booleanOrDefault(meta, false);\n        }\n        return ritual;\n    }\n\n    String spellComponents() {\n        JsonNode components = SpellFields.components.getFrom(rootNode);\n        if (components == null) {\n            return \"\";\n        }\n\n        List<String> list = new ArrayList<>();\n        for (Entry<String, JsonNode> f : iterableFields(components)) {\n            switch (f.getKey().toLowerCase()) {\n                case \"v\" -> list.add(\"V\");\n                case \"s\" -> list.add(\"S\");\n                case \"m\" -> {\n                    list.add(materialComponents(f.getValue()));\n                }\n                case \"r\" -> list.add(\"R\"); // Royalty. Acquisitions Incorporated\n            }\n        }\n        return String.join(\", \", list);\n    }\n\n    String materialComponents(JsonNode source) {\n        return \"M (%s)\".formatted(\n                source.isObject()\n                        ? SpellFields.text.replaceTextFrom(source, this)\n                        : replaceText(source.asText()));\n    }\n\n    String spellDuration() {\n        StringBuilder result = new StringBuilder();\n        JsonNode durations = SpellFields.duration.ensureArrayIn(rootNode);\n        if (durations.size() > 0) {\n            addDuration(durations.get(0), result);\n        }\n        if (durations.size() > 1) {\n            JsonNode ends = durations.get(1);\n            result.append(\", \");\n            String type = SpellFields.type.getTextOrEmpty(ends);\n            if (\"timed\".equals(type)) {\n                result.append(\"up to \");\n            }\n            addDuration(ends, result);\n        }\n        return result.toString();\n    }\n\n    void addDuration(JsonNode element, StringBuilder result) {\n        String type = SpellFields.type.getTextOrEmpty(element);\n        switch (type) {\n            case \"instant\" -> result.append(\"Instantaneous\");\n            case \"permanent\" -> {\n                result.append(\"Until dispelled\");\n                if (element.withArray(\"ends\").size() > 1) {\n                    result.append(\" or triggered\");\n                }\n            }\n            case \"special\" -> result.append(\"Special\");\n            case \"timed\" -> {\n                if (booleanOrDefault(element, \"concentration\", false)) {\n                    result.append(\"Concentration, up to \");\n                }\n                JsonNode duration = element.get(\"duration\");\n                String amount = SpellFields.amount.getTextOrEmpty(duration);\n                result.append(amount)\n                        .append(\" \")\n                        .append(pluralize(\n                                SpellFields.type.getTextOrEmpty(duration),\n                                Integer.valueOf(amount)));\n            }\n            default -> tui().errorf(\"What is this? %s\", element.toPrettyString());\n        }\n    }\n\n    String spellRange() {\n        StringBuilder result = new StringBuilder();\n        JsonNode range = SpellFields.range.getFrom(rootNode);\n        if (range != null) {\n            String type = SpellFields.type.getTextOrEmpty(range);\n            JsonNode distance = SpellFields.distance.getFrom(range);\n            String distanceType = SpellFields.type.getTextOrEmpty(distance);\n            String amount = SpellFields.amount.getTextOrEmpty(distance);\n\n            switch (type) {\n                case \"cube\", \"cone\", \"emanation\", \"hemisphere\", \"line\", \"radius\", \"sphere\" -> {// Self (xx-foot yy)\n                    result.append(\"Self (\")\n                            .append(amount)\n                            .append(\"-\")\n                            .append(pluralize(distanceType, 1))\n                            .append(\" \")\n                            .append(uppercaseFirst(type))\n                            .append(\")\");\n                }\n                case \"point\" -> {\n                    switch (distanceType) {\n                        case \"self\", \"sight\", \"touch\", \"unlimited\" ->\n                            result.append(uppercaseFirst(distanceType));\n                        default -> result.append(amount)\n                                .append(\" \")\n                                .append(distanceType);\n                    }\n                }\n                case \"special\" -> result.append(\"Special\");\n            }\n        }\n        return result.toString();\n    }\n\n    String spellCastingTime() {\n        StringBuilder result = new StringBuilder();\n        JsonNode time = rootNode.withArray(\"time\").get(0);\n        String number = SpellFields.number.getTextOrEmpty(time);\n        String unit = SpellFields.unit.getTextOrEmpty(time);\n        String condition = replaceText(SpellFields.condition.getTextOrEmpty(time));\n        String note = replaceText(SpellFields.note.getTextOrEmpty(time));\n\n        if (\"special\".equals(unit)) {\n            result.append(\"Special\");\n            if (!condition.isEmpty()) {\n                result.append(\" (\").append(condition).append(\")\");\n            }\n            if (!note.isEmpty()) {\n                result.append(\" (\").append(note).append(\")\");\n            }\n            return result.toString();\n        }\n\n        result.append(number).append(\" \");\n        switch (unit) {\n            case \"action\", \"reaction\" ->\n                result.append(uppercaseFirst(unit));\n            case \"bonus\" ->\n                result.append(uppercaseFirst(unit))\n                        .append(\" Action\");\n            default ->\n                result.append(unit);\n        }\n        if (!condition.isEmpty()) {\n            result.append(\", \").append(condition);\n        }\n        if (!note.isEmpty()) {\n            result.append(\" (\").append(note).append(\")\");\n        }\n        return pluralize(result.toString(), number);\n    }\n\n    String spellAreaTags() {\n        return join(\", \", SpellFields.areaTags.getListOfStrings(rootNode, tui()).stream()\n                .map(tag -> AREA_TAG_LABELS.getOrDefault(tag, uppercaseFirst(tag)))\n                .filter(Objects::nonNull)\n                .distinct()\n                .toList());\n    }\n\n    String spellDamageInflict() {\n        return join(\", \", SpellFields.damageInflict.getListOfStrings(rootNode, tui()).stream()\n                .map(this::formatKeyword)\n                .filter(Objects::nonNull)\n                .distinct()\n                .toList());\n    }\n\n    String spellSavingThrows() {\n        return join(\", \", SpellFields.savingThrow.getListOfStrings(rootNode, tui()).stream()\n                .map(this::formatKeyword)\n                .filter(Objects::nonNull)\n                .distinct()\n                .toList());\n    }\n\n    String spellConditionInflict() {\n        return join(\", \", SpellFields.conditionInflict.getListOfStrings(rootNode, tui()).stream()\n                .map(this::formatKeyword)\n                .filter(Objects::nonNull)\n                .distinct()\n                .toList());\n    }\n\n    String spellAbilityChecks() {\n        return join(\", \", SpellFields.abilityCheck.getListOfStrings(rootNode, tui()).stream()\n                .map(this::formatAbility)\n                .filter(Objects::nonNull)\n                .distinct()\n                .toList());\n    }\n\n    String spellMiscTags() {\n        return join(\", \", SpellFields.miscTags.getListOfStrings(rootNode, tui()).stream()\n                .map(this::formatMiscTag)\n                .filter(Objects::nonNull)\n                .distinct()\n                .toList());\n    }\n\n    String spellAffectsCreatureTypes() {\n        return join(\", \", SpellFields.affectsCreatureType.getListOfStrings(rootNode, tui()).stream()\n                .map(this::formatKeyword)\n                .filter(Objects::nonNull)\n                .distinct()\n                .toList());\n    }\n\n    String spellConditionImmune() {\n        return join(\", \", SpellFields.conditionImmune.getListOfStrings(rootNode, tui()).stream()\n                .map(this::formatKeyword)\n                .filter(Objects::nonNull)\n                .distinct()\n                .toList());\n    }\n\n    String spellDamageImmune() {\n        return join(\", \", SpellFields.damageImmune.getListOfStrings(rootNode, tui()).stream()\n                .map(this::formatKeyword)\n                .filter(Objects::nonNull)\n                .distinct()\n                .toList());\n    }\n\n    String spellDamageResist() {\n        return join(\", \", SpellFields.damageResist.getListOfStrings(rootNode, tui()).stream()\n                .map(this::formatKeyword)\n                .filter(Objects::nonNull)\n                .distinct()\n                .toList());\n    }\n\n    String spellDamageVulnerable() {\n        return join(\", \", SpellFields.damageVulnerable.getListOfStrings(rootNode, tui()).stream()\n                .map(this::formatKeyword)\n                .filter(Objects::nonNull)\n                .distinct()\n                .toList());\n    }\n\n    String spellAttacks() {\n        return join(\", \", SpellFields.spellAttack.getListOfStrings(rootNode, tui()).stream()\n                .map(this::formatSpellAttack)\n                .filter(Objects::nonNull)\n                .distinct()\n                .toList());\n    }\n\n    String spellScalingLevelDice() {\n        JsonNode node = SpellFields.scalingLevelDice.getFrom(rootNode);\n        if (node == null || node.isNull()) {\n            return null;\n        }\n\n        List<JsonNode> blocks = new ArrayList<>();\n        if (node.isArray()) {\n            for (JsonNode n : node) {\n                blocks.add(n);\n            }\n        } else if (node.isObject()) {\n            blocks.add(node);\n        } else {\n            return null;\n        }\n\n        List<String> result = new ArrayList<>();\n        for (JsonNode scalingNode : blocks) {\n            JsonNode scaling = SpellFields.scaling.getFrom(scalingNode);\n            if (scaling == null || scaling.isEmpty()) {\n                continue;\n            }\n\n            List<Entry<String, JsonNode>> entries = new ArrayList<>();\n            for (Entry<String, JsonNode> e : iterableFields(scaling)) {\n                entries.add(e);\n            }\n            entries.sort(Comparator.comparingInt(e -> Integer.parseInt(e.getKey())));\n\n            List<String> pieces = new ArrayList<>();\n            for (Entry<String, JsonNode> entry : entries) {\n                String level = entry.getKey();\n                String dice = entry.getValue().asText();\n                if (dice == null || dice.isBlank()) {\n                    continue;\n                }\n                pieces.add(\"%s level: %s\".formatted(toOrdinal(level), dice));\n            }\n\n            if (pieces.isEmpty()) {\n                continue;\n            }\n\n            String label = SpellFields.label.getTextOrEmpty(scalingNode);\n            if (label == null || label.isBlank()) {\n                result.add(String.join(\"; \", pieces));\n            } else {\n                result.add(\"%s: %s\".formatted(uppercaseFirst(label.trim()), String.join(\"; \", pieces)));\n            }\n        }\n\n        return result.isEmpty() ? null : String.join(\", \", result);\n    }\n\n    String spellHigherLevelEntries() {\n        if (!SpellFields.entriesHigherLevel.existsIn(rootNode)) {\n            return null;\n        }\n        String value = SpellFields.entriesHigherLevel.transformTextFrom(rootNode, \"\\n\", this, null).strip();\n        return value.isBlank() ? null : value;\n    }\n\n    private String formatKeyword(String value) {\n        if (value == null || value.isBlank()) {\n            return null;\n        }\n        return uppercaseFirst(value.trim());\n    }\n\n    private String formatAbility(String ability) {\n        if (ability == null || ability.isBlank()) {\n            return null;\n        }\n        return uppercaseFirst(ability.trim());\n    }\n\n    private String formatMiscTag(String tag) {\n        if (tag == null || tag.isBlank()) {\n            return null;\n        }\n        return MISC_TAG_LABELS.getOrDefault(tag, uppercaseFirst(tag.trim()));\n    }\n\n    private String formatSpellAttack(String code) {\n        if (code == null || code.isBlank()) {\n            return null;\n        }\n        return switch (code.trim()) {\n            case \"M\" -> \"Melee Spell Attack\";\n            case \"R\" -> \"Ranged Spell Attack\";\n            default -> null;\n        };\n    }\n\n    enum SpellFields implements JsonNodeReader {\n        abilityCheck,\n        affectsCreatureType,\n        amount,\n        areaTags,\n        className,\n        classSource,\n        classes,\n        components,\n        condition,\n        conditionImmune,\n        conditionInflict,\n        damageImmune,\n        damageInflict,\n        damageResist,\n        damageVulnerable,\n        distance,\n        duration,\n        entriesHigherLevel,\n        label,\n        level,\n        meta,\n        miscTags,\n        note,\n        number,\n        range,\n        ritual,\n        savingThrow,\n        scaling,\n        scalingLevelDice,\n        school,\n        self,\n        sight,\n        special,\n        spellAttack,\n        text,\n        touch,\n        type,\n        unit,\n        unlimited,\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteSpellIndex.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static dev.ebullient.convert.StringUtil.isPresent;\nimport static dev.ebullient.convert.StringUtil.toTitleCase;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.TreeSet;\n\nimport dev.ebullient.convert.qute.QuteNote;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteNote;\n\n/**\n * Read the spell index: create a variety of lists and indexes for spells\n */\npublic class Json2QuteSpellIndex extends Json2QuteCommon {\n    static final List<String> SPELL_LEVELS = List.of(\n            \"Cantrip\", \"1st Level\", \"2nd Level\", \"3rd Level\", \"4th Level\", \"5th Level\", \"6th Level\", \"7th Level\", \"8th Level\",\n            \"9th Level\");\n\n    final SpellIndex spellIndex;\n\n    Json2QuteSpellIndex(Tools5eIndex index) {\n        super(index, Tools5eIndexType.spellIndex, null);\n        this.spellIndex = index.getSpellIndex();\n    }\n\n    public Collection<? extends QuteNote> buildNotes() {\n        List<QuteNote> notes = new ArrayList<>();\n\n        SpellByLevel spellsByLevel = new SpellByLevel();\n        Map<String, SpellByLevel> spellsByClass = new HashMap<>();\n        Map<String, SpellRefByLevel> spellsByOther = new HashMap<>();\n        Map<SpellSchool, SpellByLevel> spellsBySchool = new HashMap<>();\n\n        // Spells by all the things.\n        for (var entry : spellIndex.spellsByKey.values()) {\n            spellsByLevel.add(entry);\n            spellsBySchool.computeIfAbsent(entry.school, k -> new SpellByLevel()).add(entry);\n\n            for (var name : entry.classes) {\n                spellsByClass.computeIfAbsent(name, k -> new SpellByLevel()).add(entry);\n            }\n\n            for (var ref : entry.references.values()) {\n                if (ref.refererType == Tools5eIndexType.classtype) {\n                    continue;\n                }\n                spellsByOther.computeIfAbsent(ref.refererKey, k -> new SpellRefByLevel(ref))\n                        .add(entry);\n            }\n\n            for (var ref : entry.expandedList.values()) {\n                if (ref.refererType == Tools5eIndexType.classtype) {\n                    continue;\n                }\n                spellsByOther.computeIfAbsent(ref.refererKey, k -> new SpellRefByLevel(ref))\n                        .add(entry);\n            }\n        }\n\n        // Create school spell list\n        for (var schoolList : spellsBySchool.entrySet()) {\n            QuteNote note = createSchoolList(schoolList.getKey(), schoolList.getValue());\n            if (note != null) {\n                notes.add(note);\n            }\n        }\n\n        // Create class spell list\n        for (var classList : spellsByClass.entrySet()) {\n            QuteNote note = createClassList(classList.getKey(), classList.getValue());\n            if (note != null) {\n                notes.add(note);\n            }\n        }\n\n        // Create other spell list\n        for (var otherList : spellsByOther.entrySet()) {\n            QuteNote note = createOtherList(otherList.getKey(), otherList.getValue());\n            if (note != null) {\n                notes.add(note);\n            }\n        }\n\n        return notes;\n    }\n\n    private QuteNote createClassList(String className, SpellByLevel spellsByClass) {\n        if (spellsByClass.isEmpty()) {\n            return null;\n        }\n\n        Tags tags = new Tags();\n        tags.add(\"spell\", \"list\", \"class\", className);\n        List<String> text = new ArrayList<>();\n\n        for (int i = 0; i < SPELL_LEVELS.size(); i++) {\n            Set<SpellEntry> levelSpells = spellsByClass.getLevel(String.valueOf(i));\n            if (levelSpells.isEmpty()) {\n                continue;\n            }\n            maybeAddBlankLine(text);\n            String levelHeading = SPELL_LEVELS.get(i);\n            text.add(\"## \" + levelHeading);\n            text.add(\"\");\n            for (var entry : levelSpells) {\n                text.add(\"- \" + entry.linkify() + (entry.isExpanded(className) ? \" (\\\\*)\" : \"\"));\n            }\n        }\n        maybeAddBlankLine(text);\n\n        return new Tools5eQuteNote(toTitleCase(className) + \" Spells\", \"\", text, tags)\n                .withTargetFile(linkifier().getClassSpellList(className))\n                .withTargetPath(linkifier().getRelativePath(Tools5eIndexType.spellIndex));\n    }\n\n    private QuteNote createSchoolList(SpellSchool spellSchool, SpellByLevel spellsByClass) {\n        if (spellsByClass.isEmpty()) {\n            return null;\n        }\n\n        Tags tags = new Tags();\n        tags.add(\"spell\", \"list\", \"school\", spellSchool.name());\n        List<String> text = new ArrayList<>();\n\n        for (int i = 0; i < SPELL_LEVELS.size(); i++) {\n            Set<SpellEntry> levelSpells = spellsByClass.getLevel(String.valueOf(i));\n            if (levelSpells.isEmpty()) {\n                continue;\n            }\n            maybeAddBlankLine(text);\n            String levelHeading = SPELL_LEVELS.get(i);\n            text.add(\"## \" + levelHeading);\n            text.add(\"\");\n            for (var entry : levelSpells) {\n                text.add(\"- \" + entry.linkify());\n            }\n        }\n        maybeAddBlankLine(text);\n\n        return new Tools5eQuteNote(spellSchool.name() + \" Spells\", \"\", text, tags)\n                .withTargetFile(\"list-spells-school-\" + spellSchool.name())\n                .withTargetPath(linkifier().getRelativePath(Tools5eIndexType.spellIndex));\n    }\n\n    private QuteNote createOtherList(String key, SpellRefByLevel spellsByOther) {\n        if (spellsByOther.isEmpty()) {\n            return null;\n        }\n        Tools5eIndexType type = Tools5eIndexType.getTypeFromKey(key);\n        String name = linkifier().decoratedName(type, spellsByOther.reference.refererNode);\n\n        Tags tags = new Tags();\n        tags.add(\"spell\", \"list\", type.name(), name);\n        List<String> text = new ArrayList<>();\n\n        for (int i = 0; i < SPELL_LEVELS.size(); i++) {\n            Set<SpellEntry> levelSpells = spellsByOther.getLevel(String.valueOf(i));\n            if (levelSpells.isEmpty()) {\n                continue;\n            }\n            maybeAddBlankLine(text);\n            String levelHeading = SPELL_LEVELS.get(i);\n            text.add(\"## \" + levelHeading);\n            text.add(\"\");\n            for (var entry : levelSpells) {\n                SpellEntry.SpellReference spellRef = entry.getMostSpecificReference(key);\n                String desc = spellRef != null ? spellRef.describe() : \"\";\n                text.add(\"- \" + entry.linkify() + (isPresent(desc) ? \" \" + desc : \"\"));\n            }\n        }\n        maybeAddBlankLine(text);\n\n        return new Tools5eQuteNote(\"Spells for \" + name, \"\", text, tags)\n                .withTargetFile(spellsByOther.reference.listFileName())\n                .withTargetPath(linkifier().getRelativePath(Tools5eIndexType.spellIndex));\n    }\n\n    private class SpellRefByLevel extends SpellByLevel {\n        final SpellEntry.SpellReference reference;\n\n        SpellRefByLevel(SpellEntry.SpellReference reference) {\n            this.reference = reference;\n        }\n\n        boolean isEmpty() {\n            return spellsByLevel.isEmpty();\n        }\n    }\n\n    private class SpellByLevel {\n        final Map<String, Set<SpellEntry>> spellsByLevel = new HashMap<>();\n\n        void add(SpellEntry entry) {\n            spellsByLevel.computeIfAbsent(entry.getLevel(),\n                    k -> new TreeSet<>(Comparator.comparing(SpellEntry::getName))).add(entry);\n        }\n\n        Set<SpellEntry> getLevel(String level) {\n            Set<SpellEntry> spells = spellsByLevel.get(level);\n            return spells == null ? Set.of() : spells;\n        }\n\n        boolean isEmpty() {\n            return spellsByLevel.isEmpty();\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteTable.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.ArrayNode;\n\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteNote;\n\npublic class Json2QuteTable extends Json2QuteCommon {\n\n    Json2QuteTable(Tools5eIndex index, Tools5eIndexType type, JsonNode jsonNode) {\n        super(index, type, jsonNode);\n    }\n\n    @Override\n    public Tools5eQuteNote buildQuteNote() {\n        String key = getSources().getKey();\n        if (index.isExcluded(key)) {\n            return null;\n        }\n\n        Tags tags = new Tags(getSources());\n        List<String> text = new ArrayList<>();\n        String targetDir = linkifier().getRelativePath(type);\n        String targetFile = null;\n\n        if (getName().equals(\"Damage Types\")) {\n            for (JsonNode row : iterableElements(rootNode.get(\"rows\"))) {\n                ArrayNode cols = (ArrayNode) row;\n                maybeAddBlankLine(text);\n                text.add(\"## \" + replaceText(cols.get(0).asText()));\n                maybeAddBlankLine(text);\n                appendToText(text, cols.get(1), null);\n            }\n            targetDir = null;\n        } else if (type == Tools5eIndexType.tableGroup) {\n            appendToText(text, Tools5eFields.tables.getFrom(rootNode), \"##\");\n        } else {\n            targetFile = linkifier().getTargetFileName(getName(), getSources());\n            appendTable(text, rootNode);\n        }\n\n        return new Tools5eQuteNote(getName(), getSourceText(sources), text, tags)\n                .withTargetPath(targetDir)\n                .withTargetFile(targetFile);\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/Json2QuteVehicle.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static dev.ebullient.convert.StringUtil.toTitleCase;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Optional;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.qute.NamedText;\nimport dev.ebullient.convert.tools.JsonNodeReader;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.qute.AcHp;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteVehicle;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteVehicle.ShipAcHp;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteVehicle.ShipCrewCargoPace;\nimport dev.ebullient.convert.tools.dnd5e.qute.QuteVehicle.ShipSection;\nimport dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase;\n\npublic class Json2QuteVehicle extends Json2QuteCommon {\n\n    final VehicleType vehicleType;\n\n    Json2QuteVehicle(Tools5eIndex index, Tools5eIndexType type, JsonNode jsonNode) {\n        super(index, type, jsonNode);\n        vehicleType = VehicleType.from(rootNode);\n    }\n\n    @Override\n    protected Tools5eQuteBase buildQuteResource() {\n        Tags tags = new Tags(getSources());\n        tags.add(\"vehicle\", \"type\", vehicleType.value);\n\n        List<String> terrain = VehicleFields.terrain.replaceTextFromList(rootNode, index);\n        terrain.forEach(x -> tags.add(\"vehicle\", \"terrain\", x));\n\n        List<ImageRef> fluffImages = new ArrayList<>();\n        List<String> text = getFluff(Tools5eIndexType.vehicleFluff, \"##\", fluffImages);\n        appendToText(text, SourceField.entries.getFrom(rootNode), \"##\");\n\n        return new QuteVehicle(sources,\n                getSources().getName(),\n                getSourceText(sources),\n                vehicleType.name(),\n                String.join(\", \", terrain),\n                abilityScores(rootNode),\n                vehicleSize(tags),\n                immuneResist(),\n                shipCrewCargoPace(),\n                shipSections(),\n                getActions(),\n                getToken(), fluffImages,\n                String.join(\"\\n\", text),\n                tags);\n    }\n\n    String vehicleSize(Tags tags) {\n        String dimensions = \"\";\n        String weight = \"\";\n        if (VehicleFields.dimensions.existsIn(rootNode)) {\n            dimensions = \" (\" + VehicleFields.dimensions.joinAndReplace(rootNode, this, \" by \") + \")\";\n        }\n        if (VehicleFields.weight.existsIn(rootNode)) {\n            weight += \" (\" + poundsToTons(VehicleFields.weight.getFrom(rootNode)) + \")\";\n        }\n        if (Tools5eFields.size.existsIn(rootNode)) {\n            String size = getSize(rootNode);\n            tags.add(\"vehicle\", \"size\", size);\n\n            return size + \" vehicle\" + dimensions + weight;\n        } else if (!dimensions.isBlank()) {\n            return toTitleCase(vehicleType.value) + dimensions + weight;\n        }\n        return null;\n    }\n\n    String vehicleSpeed() {\n        if (vehicleType == VehicleType.INFWAR) {\n            return speed(Tools5eFields.speed.getFrom(rootNode));\n        }\n        return \"\";\n    }\n\n    private List<NamedText> getActions() {\n        List<NamedText> actions = collectTraits(\"action\");\n\n        if (VehicleFields.other.existsIn(rootNode)) {\n            for (JsonNode node : VehicleFields.other.iterateArrayFrom(rootNode)) {\n                if (SourceField.name.getTextOrEmpty(node).equals(\"Actions\")) {\n                    addNamedTrait(actions, \"\", node);\n                }\n            }\n        }\n        return actions;\n    }\n\n    private ShipAcHp getAcHp(JsonNode node) {\n        String cost = getCost(node);\n        return new ShipAcHp(vehicleType.name(),\n                VehicleFields.ac.intOrNull(node),\n                VehicleFields.acFrom.getTextOrNull(node),\n                VehicleFields.hp.intOrNull(node),\n                VehicleFields.hpNote.getTextOrNull(node),\n                VehicleFields.dt.intOrNull(node),\n                null,\n                cost == null ? null : cost.toString());\n    }\n\n    private String getCost(JsonNode node) {\n        String cost = convertCost(node);\n        if (VehicleFields.costs.existsIn(node)) {\n            List<String> costs = new ArrayList<>();\n            for (JsonNode costNode : VehicleFields.costs.iterateArrayFrom(node)) {\n                costs.add(convertCost(costNode));\n            }\n            cost = String.join(\", \", costs);\n        }\n        return cost;\n    }\n\n    private String convertCost(JsonNode node) {\n        Optional<Integer> costCp = VehicleFields.cost.intFrom(node);\n        String note = VehicleFields.note.getTextOrNull(node);\n        if (costCp.isPresent() || note != null) {\n            return costCp.map(x -> convertCurrency(x)).orElse(\"\\u23E4\") + (note == null ? \"\" : \" (\" + note + \")\");\n        }\n        return null;\n    }\n\n    private ShipCrewCargoPace shipCrewCargoPace() {\n        Integer shipPace = null;\n        String speedPace = \"\";\n        ShipAcHp shipAcHp = null;\n        String keelBeam = null;\n\n        if (vehicleType == VehicleType.SHIP) {\n            shipPace = VehicleFields.pace.intOrNull(rootNode);\n        } else if (vehicleType == VehicleType.SPELLJAMMER) {\n            JsonNode speedNode = VehicleFields.speed.getFrom(rootNode);\n            JsonNode paceNode = VehicleFields.pace.getFrom(rootNode);\n\n            String speed = speed(speedNode, false);\n            List<String> text = new ArrayList<>();\n            for (String k : SPEED_MODE) {\n                JsonNode pNode = paceNode.get(k);\n                if (pNode != null) {\n                    String prefix = \"walk\".equals(k) ? \"\" : k + \" \";\n                    double num = convertToNumber(pNode.asText());\n                    text.add(prefix + pNode.asText() + \" mph ^[\" + (num * 24) + \" miles per day]\");\n                }\n            }\n            String pace = text.isEmpty() ? \"\" : \" (\" + String.join(\", \", text) + \")\";\n            speedPace = speed + pace;\n\n            JsonNode hull = VehicleFields.hull.getFrom(rootNode);\n            shipAcHp = getAcHp(hull);\n            // Replace the cost with the value from the root node\n            shipAcHp.cost = getCost(rootNode);\n\n            if (VehicleFields.dimensions.existsIn(rootNode)) {\n                keelBeam = VehicleFields.dimensions.joinAndReplace(rootNode, this, \" by \");\n            }\n        } else if (vehicleType == VehicleType.INFWAR) {\n            int dexMod = VehicleFields.dexMod.intOrDefault(rootNode, 0);\n            JsonNode hpNode = VehicleFields.hp.getFrom(rootNode);\n            shipAcHp = new ShipAcHp(vehicleType.name(),\n                    dexMod == 0 ? 19 : 19 + dexMod,\n                    dexMod == 0 ? \"\" : \"19 while motionless\",\n                    VehicleFields.hp.intOrNull(hpNode),\n                    null,\n                    VehicleFields.dt.intOrNull(hpNode),\n                    VehicleFields.mt.intOrNull(hpNode),\n                    null);\n            speedPace = speed(Tools5eFields.speed.getFrom(rootNode));\n        } else if (vehicleType == VehicleType.CREATURE || vehicleType == VehicleType.OBJECT) {\n            AcHp creatureAcHp = new AcHp();\n            findAc(creatureAcHp);\n            findHp(creatureAcHp);\n            shipAcHp = new ShipAcHp(vehicleType.name(), creatureAcHp);\n            speedPace = speed(Tools5eFields.speed.getFrom(rootNode));\n        }\n\n        String capCrew = VehicleFields.capCrew.getTextOrEmpty(rootNode);\n        if (!capCrew.isBlank()) {\n            capCrew = capCrew + (vehicleType == VehicleType.INFWAR\n                    ? \" Medium creatures\"\n                    : \" crew\");\n        }\n\n        String cargo = \"\";\n        if (VehicleFields.capCargo.existsIn(rootNode)) {\n            JsonNode cargoNode = VehicleFields.capCargo.getFrom(rootNode);\n            if (cargoNode.isTextual()) {\n                cargo = replaceText(cargoNode.asText());\n            } else if (vehicleType == VehicleType.INFWAR) {\n                cargo = poundsToTons(cargoNode);\n            } else {\n                String txt = cargoNode.asText();\n                cargo = txt + \" ton\" + (\"1\".equals(txt) ? \"\" : \"s\");\n            }\n        }\n\n        return new ShipCrewCargoPace(vehicleType.name(),\n                capCrew,\n                VehicleFields.capCrewNote.replaceTextFrom(rootNode, this),\n                VehicleFields.capPassenger.getTextOrEmpty(rootNode),\n                cargo,\n                shipPace, speedPace, shipAcHp, keelBeam);\n    }\n\n    private String poundsToTons(JsonNode sourceNode) {\n        int lbs = sourceNode.asInt();\n        int tons = lbs / 2000;\n        lbs %= 2000;\n        return (tons == 0 ? \"\" : tons + \" ton\" + (tons == 1 ? \"\" : \"s\")) + (lbs == 0 ? \"\" : \" \" + lbs + \" lb.\");\n    }\n\n    private List<ShipSection> shipSections() {\n        List<ShipSection> sections = new ArrayList<>();\n        switch (vehicleType) {\n            case SHIP -> getShipSections(sections);\n            case SPELLJAMMER -> getSpelljammerSections(sections);\n            case INFWAR -> getWarMachineSections(sections);\n            case CREATURE -> getCreatureSections(sections);\n            default -> {\n            }\n        }\n        return sections;\n    }\n\n    void getShipSections(List<ShipSection> sections) {\n        if (VehicleFields.hull.existsIn(rootNode)) {\n            JsonNode hull = VehicleFields.hull.getFrom(rootNode);\n            sections.add(new ShipSection(\"Hull\",\n                    getAcHp(hull), null, collectEntries(hull), null));\n        }\n        if (VehicleFields.trait.existsIn(rootNode)) {\n            sections.add(new ShipSection(\"Traits\",\n                    null, null,\n                    collectSortedEntries(rootNode, VehicleFields.trait),\n                    null));\n        }\n        for (JsonNode node : VehicleFields.control.iterateArrayFrom(rootNode)) {\n            String name = SourceField.name.replaceTextFrom(node, this);\n            sections.add(new ShipSection(\"Control: \" + name,\n                    getAcHp(node), null, collectEntries(node), null));\n        }\n        for (JsonNode node : VehicleFields.movement.iterateArrayFrom(rootNode)) {\n            String name = SourceField.name.replaceTextFrom(node, this);\n            boolean isControl = VehicleFields.isControl.booleanOrDefault(node, false);\n\n            List<String> speed = new ArrayList<>();\n            addLocomotion(speed, VehicleFields.locomotion.getFrom(node));\n            addMovementSpeed(speed, VehicleFields.speed.getFrom(node));\n\n            sections.add(new ShipSection((isControl ? \"Control and movement: \" : \"Movement: \") + name,\n                    getAcHp(node), speed, collectEntries(node), null));\n        }\n        for (JsonNode node : VehicleFields.weapon.iterateArrayFrom(rootNode)) {\n            String name = SourceField.name.replaceTextFrom(node, this);\n            Optional<Integer> count = VehicleFields.count.intFrom(node);\n            if (count.isPresent()) {\n                name += \" (\" + count.get() + \")\";\n            }\n\n            sections.add(new ShipSection(\"Weapon: \" + name,\n                    getAcHp(node), null, collectEntries(node), null));\n        }\n        if (VehicleFields.other.existsIn(rootNode)) {\n            for (JsonNode node : VehicleFields.other.iterateArrayFrom(rootNode)) {\n                String name = SourceField.name.replaceTextFrom(node, this);\n                if (!name.isBlank() && !name.equals(\"Actions\")) {\n                    sections.add(new ShipSection(name,\n                            getAcHp(node), null, collectEntries(node), null));\n                }\n            }\n        }\n    }\n\n    void addLocomotion(List<String> speed, JsonNode locomotionNode) {\n        for (JsonNode node : iterableElements(locomotionNode)) {\n            String mode = VehicleFields.mode.getTextOrEmpty(node);\n            speed.add(new NamedText(\"Locomotion (\" + mode + \").\",\n                    collectEntries(node)).toString());\n        }\n    }\n\n    void addMovementSpeed(List<String> speed, JsonNode speedNode) {\n        for (JsonNode node : iterableElements(speedNode)) {\n            String mode = VehicleFields.mode.getTextOrEmpty(node);\n            speed.add(new NamedText(\"Speed (\" + mode + \").\",\n                    collectEntries(node)).toString());\n        }\n    }\n\n    private void getSpelljammerSections(List<ShipSection> sections) {\n        for (JsonNode node : VehicleFields.weapon.iterateArrayFrom(rootNode)) {\n            String name = SourceField.name.replaceTextFrom(node, this);\n            Optional<Integer> count = VehicleFields.count.intFrom(node);\n            Optional<Integer> crew = VehicleFields.crew.intFrom(node);\n            boolean isMultiple = count.isPresent() && count.get() > 1;\n\n            if (isMultiple) {\n                name = count.get() + \" \" + name;\n            }\n            if (crew.isPresent()) {\n                name += \" (Crew: \" + crew.get() + (isMultiple ? \" each\" : \"\") + \")\";\n            }\n\n            List<String> actions = List.of();\n            if (VehicleFields.action.existsIn(node)) {\n                actions = collectNamedEntries(node, VehicleFields.action);\n            }\n\n            sections.add(new ShipSection(\"Weapon: \" + name,\n                    getAcHp(node), null, collectEntries(node), actions));\n        }\n    }\n\n    private void getWarMachineSections(List<ShipSection> sections) {\n        if (VehicleFields.trait.existsIn(rootNode)) {\n            sections.add(new ShipSection(\"Traits\",\n                    null, null,\n                    collectSortedEntries(rootNode, VehicleFields.trait),\n                    null));\n        }\n        if (VehicleFields.actionStation.existsIn(rootNode)) {\n            sections.add(new ShipSection(\"Action Stations\",\n                    null, null,\n                    collectNamedEntries(rootNode, VehicleFields.actionStation),\n                    null));\n        }\n        if (VehicleFields.reaction.existsIn(rootNode)) {\n            sections.add(new ShipSection(\"Reactions\",\n                    null, null,\n                    collectNamedEntries(rootNode, VehicleFields.reaction),\n                    null));\n        }\n    }\n\n    private void getCreatureSections(List<ShipSection> sections) {\n        if (VehicleFields.trait.existsIn(rootNode)) {\n            sections.add(new ShipSection(\"Traits\",\n                    null, null,\n                    collectSortedEntries(rootNode, VehicleFields.trait),\n                    null));\n        }\n        if (VehicleFields.action.existsIn(rootNode)) {\n            sections.add(new ShipSection(\"Actions\",\n                    null, null,\n                    collectSortedEntries(rootNode, VehicleFields.action),\n                    null));\n        }\n        if (VehicleFields.bonus.existsIn(rootNode)) {\n            sections.add(new ShipSection(\"Bonus Actions\",\n                    null, null,\n                    collectSortedEntries(rootNode, VehicleFields.bonus),\n                    null));\n        }\n        if (VehicleFields.reaction.existsIn(rootNode)) {\n            sections.add(new ShipSection(\"Reactions\",\n                    null, null,\n                    collectSortedEntries(rootNode, VehicleFields.reaction),\n                    null));\n        }\n        if (VehicleFields.legendary.existsIn(rootNode)) {\n            sections.add(new ShipSection(\"Legendary\",\n                    null, null,\n                    collectNamedEntries(rootNode, VehicleFields.legendary),\n                    null));\n        }\n        if (VehicleFields.mythic.existsIn(rootNode)) {\n            sections.add(new ShipSection(\"Mythic Actions\",\n                    null, null,\n                    collectNamedEntries(rootNode, VehicleFields.mythic),\n                    null));\n        }\n    }\n\n    private List<String> collectNamedEntries(JsonNode source, JsonNodeReader field) {\n        List<NamedText> namedText = new ArrayList<>();\n        collectTraits(namedText, field.getFrom(source));\n        return namedText.stream().map(NamedText::toString).toList();\n    }\n\n    private List<String> collectSortedEntries(JsonNode source, JsonNodeReader field) {\n        Collection<NamedText> namedText = collectSortedTraits(field.getFrom(source));\n        return namedText.stream().map(NamedText::toString).toList();\n    }\n\n    enum VehicleType {\n        SHIP(\"Ship\"),\n        ELEMENTAL_AIRSHIP(\"Elemental Airship\"),\n        SPELLJAMMER(\"Spelljammer\"),\n        INFWAR(\"Infernal War Machine\"),\n        CREATURE(\"Creature\"),\n        OBJECT(\"Object\");\n\n        final String value;\n\n        VehicleType(String value) {\n            this.value = value;\n        }\n\n        static VehicleType from(JsonNode node) {\n            String vehicleType = VehicleFields.vehicleType.getTextOrDefault(node, \"SHIP\");\n            return switch (vehicleType) {\n                case \"SHIP\" -> SHIP;\n                case \"ELEMENTAL_AIRSHIP\" -> ELEMENTAL_AIRSHIP;\n                case \"SPELLJAMMER\" -> SPELLJAMMER;\n                case \"INFWAR\" -> INFWAR;\n                case \"CREATURE\" -> CREATURE;\n                case \"OBJECT\" -> OBJECT;\n                default -> throw new IllegalArgumentException(\"Unexpected vehicle type: \" + vehicleType);\n            };\n        }\n    }\n\n    enum VehicleFields implements JsonNodeReader {\n        ac,\n        acFrom,\n        action,\n        actionStation,\n        capCargo,\n        capCreature,\n        capCrew,\n        capCrewNote,\n        capPassenger,\n        control,\n        cost,\n        costs,\n        count,\n        crew,\n        dimensions,\n        dt,\n        hp,\n        hpNote,\n        hull,\n        isControl,\n        locomotion,\n        mode,\n        movement,\n        note,\n        other,\n        pace,\n        reaction,\n        size,\n        speed,\n        terrain,\n        trait,\n        vehicleType,\n        weapon,\n        weight,\n        dexMod,\n        mt,\n        bonus,\n        mythic,\n        legendary,\n        lairActions,\n        regionalEffects,\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/JsonSource.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static dev.ebullient.convert.StringUtil.isPresent;\nimport static dev.ebullient.convert.StringUtil.joinConjunct;\nimport static dev.ebullient.convert.StringUtil.markdownLinkToHtml;\nimport static java.util.Map.entry;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.ArrayNode;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport com.fasterxml.jackson.databind.node.TextNode;\n\nimport dev.ebullient.convert.io.Msg;\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.qute.SourceAndPage;\nimport dev.ebullient.convert.tools.JsonNodeReader;\nimport dev.ebullient.convert.tools.JsonSourceCopier.MetaFields;\nimport dev.ebullient.convert.tools.JsonTextConverter;\nimport dev.ebullient.convert.tools.ParseState;\nimport dev.ebullient.convert.tools.ToolsIndex.TtrpgValue;\nimport dev.ebullient.convert.tools.dnd5e.HomebrewIndex.HomebrewMetaTypes;\nimport dev.ebullient.convert.tools.dnd5e.Json2QuteClass.ClassFeature;\nimport dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase;\nimport io.quarkus.runtime.annotations.RegisterForReflection;\n\npublic interface JsonSource extends JsonTextReplacement {\n    int CR_UNKNOWN = 100001;\n    int CR_CUSTOM = 100000;\n    Pattern leadingNumber = Pattern.compile(\"(\\\\d+)(.*)\");\n\n    default String getName() {\n        return getSources() == null ? null : getSources().getName();\n    }\n\n    default boolean textContains(List<String> haystack, String needle) {\n        return haystack.stream().anyMatch(x -> x.contains(needle));\n    }\n\n    default String getTextOrEmpty(JsonNode x, String field) {\n        if (x.has(field)) {\n            return x.get(field).asText();\n        }\n        return \"\";\n    }\n\n    default String getTextOrDefault(JsonNode x, String field, String value) {\n        if (x.has(field)) {\n            return x.get(field).asText();\n        }\n        return value;\n    }\n\n    default boolean booleanOrDefault(JsonNode source, String key, boolean value) {\n        JsonNode result = source.get(key);\n        return result == null ? value : result.asBoolean(value);\n    }\n\n    default int intOrDefault(JsonNode source, String key, int value) {\n        JsonNode result = source.get(key);\n        return result == null ? value : result.asInt();\n    }\n\n    default int intOrThrow(JsonNode source, String key) {\n        JsonNode result = source.get(key);\n        if (result == null || !result.canConvertToInt()) {\n            tui().errorf(\"Missing required field, or field is not a number. Key: %s; value: %s; from %s: %s\",\n                    key, result, getSources(), source);\n            return -999;\n        }\n        return result.asInt();\n    }\n\n    default String getSourceText(JsonNode node) {\n        return getSourceText(Tools5eSources.findOrTemporary(node));\n    }\n\n    default String getSourceText(ParseState parseState) {\n        return parseState().longSourcePageString(\"_Source: %s_\");\n    }\n\n    default String getSourceText(Tools5eSources currentSource) {\n        return currentSource.getSourceText();\n    }\n\n    default String getLabeledSource(JsonNode node) {\n        return getLabeledSource(Tools5eSources.findOrTemporary(node));\n    }\n\n    default String getLabeledSource(Tools5eSources currentSource) {\n        return \"_Source: \" + getSourceText(currentSource) + \"_\";\n    }\n\n    default ImageRef buildImageRef(JsonMediaHref mediaHref, String imageBasePath) {\n        return getSources().buildImageRef(index(), mediaHref, imageBasePath, useCompendium());\n    }\n\n    default String getFileName() {\n        return linkifier().getTargetFileName(getName(), getSources());\n    }\n\n    /**\n     * External (and recursive) entry point for content parsing.\n     * Parse attributes of the given node and add resulting lines\n     * to the provided list.\n     *\n     * @param text Parsed content is appended to this list\n     * @param node Textual, Array, or Object node containing content to parse/render\n     * @param heading The current header depth and/or if headings are allowed for this text element\n     */\n    @Override\n    default void appendToText(List<String> text, JsonNode node, String heading) {\n        boolean pushed = parseState().push(node); // store state\n        try {\n            if (node == null || node.isNull()) {\n                // do nothing\n            } else if (node.isTextual()) {\n                text.add(replaceText(node.asText()));\n            } else if (node.isNumber() || node.isBoolean()) {\n                text.add(node.asText());\n            } else if (node.isArray()) {\n                for (JsonNode f : iterableElements(node)) {\n                    maybeAddBlankLine(text);\n                    appendToText(text, f, heading);\n                }\n            } else if (node.isObject()) {\n                appendObjectToText(text, node, heading);\n            } else {\n                tui().debugf(Msg.UNKNOWN, \"Unknown entry type in %s: %s\", getSources(), node.toPrettyString());\n            }\n        } finally {\n            parseState().pop(pushed); // restore state\n        }\n    }\n\n    default void appendObjectToText(List<String> text, JsonNode node, String heading) {\n        AppendTypeValue type = AppendTypeValue.valueFrom(node, SourceField.type);\n        String source = SourceField.source.getTextOrEmpty(node);\n\n        // entriesOtherSource handled here.\n        if (!source.isEmpty() && !cfg().sourceIncluded(source)) {\n            if (!getSources().includedByConfig()) {\n                return;\n            }\n        }\n\n        boolean pushed = parseState().push(node);\n        try {\n            if (type != null) {\n                switch (type) {\n                    case attack -> appendAttack(text, node);\n                    case entries, section -> appendEntriesToText(text, node, heading);\n                    case entry, item, itemSpell, itemSub -> appendEntryItem(text, node);\n                    case abilityDc, abilityAttackMod, abilityGeneric -> appendAbility(type, text, node);\n                    case flowchart -> appendFlowchart(text, node, heading);\n                    case gallery -> appendGallery(text, node);\n                    case homebrew -> appendCallout(\"note\", \"Homebrew\", text, node);\n                    case hr -> {\n                        maybeAddBlankLine(text);\n                        text.add(\"---\");\n                        text.add(\"\");\n                    }\n                    case image -> appendImage(text, node);\n                    case inline, inlineBlock -> {\n                        List<String> inner = new ArrayList<>();\n                        appendToText(inner, SourceField.entries.getFrom(node), null);\n                        text.add(String.join(\"\", inner));\n                    }\n                    case inset, insetReadaloud -> appendInset(type, text, node);\n                    case link -> appendLink(text, node);\n                    case list -> {\n                        String style = Tools5eFields.style.getTextOrEmpty(node);\n                        if (\"list-no-bullets\".equals(style)) {\n                            if (node.has(\"columns\")) {\n                                maybeAddBlankLine(text);\n                                appendToText(text, SourceField.items.readArrayFrom(node), heading);\n                            } else {\n                                appendList(text, SourceField.items.readArrayFrom(node), ListType.unstyled);\n                            }\n                        } else {\n                            appendList(text, SourceField.items.readArrayFrom(node), ListType.unordered);\n                        }\n                    }\n                    case optfeature -> appendOptionalFeature(text, node, heading);\n                    case options -> appendOptions(text, node);\n                    case quote -> appendQuote(text, node);\n                    case refClassFeature -> appendClassFeatureRef(text, node, Tools5eIndexType.classfeature, \"classFeature\");\n                    case refOptionalfeature -> appendOptionalFeatureRef(text, node);\n                    case refSubclassFeature -> appendClassFeatureRef(text, node, Tools5eIndexType.subclassFeature,\n                            \"subclassFeature\");\n                    case statblock -> appendStatblock(text, node, heading);\n                    case statblockInline -> appendStatblockInline(text, node, heading);\n                    case table -> appendTable(text, node);\n                    case tableGroup -> appendTableGroup(text, node, heading);\n                    case variant -> appendCallout(\"danger\", \"Variant\", text, node);\n                    case variantInner, variantSub -> appendCallout(\"example\", \"Variant\", text, node);\n                    default -> tui().debugf(Msg.UNKNOWN, \"Unknown entry object type %s from %s: %s\",\n                            type, getSources(), node.toPrettyString());\n                }\n                // any entry/entries handled by type...\n                return;\n            }\n\n            appendToText(text, SourceField.entry.getFrom(node), heading);\n            appendToText(text, SourceField.entries.getFrom(node), heading);\n\n            JsonNode additionalEntries = Tools5eFields.additionalEntries.getFrom(node);\n            if (additionalEntries != null) {\n                for (JsonNode entry : iterableElements(additionalEntries)) {\n                    String entrySource = SourceField.source.getTextOrNull(entry);\n                    if (entrySource != null && !cfg().sourceIncluded(getSources())) {\n                        return;\n                    }\n                    appendToText(text, entry, heading);\n                }\n            }\n        } catch (RuntimeException ex) {\n            tui().errorf(ex, \"Error [%s] occurred while parsing %s\", ex.getMessage(), node.toString());\n            throw ex;\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    default void appendAbility(AppendTypeValue type, List<String> text, JsonNode entry) {\n        List<String> abilities = Tools5eFields.attributes.streamFrom(entry)\n                .map(this::asAbilityEnum)\n                .toList();\n        String ability = joinConjunct(\" or \", abilities);\n\n        if (type == AppendTypeValue.abilityDc) {\n            // {\n            //     \"type\": \"abilityDc\",\n            //     \"name\": \"Spell\",\n            //     \"attributes\": [\n            //         \"cha\"\n            //     ]\n            // },\n            String dcName = SourceField.name.replaceTextFrom(entry, this);\n            text.add(spanWrap(\"abilityDc\",\n                    getSources().isClassic()\n                            ? \"**%s save DC**: your proficiency bonus + your %s\"\n                                    .formatted(dcName, ability)\n                            : \"**%s save DC**: %s + Proficiency Bonus\"\n                                    .formatted(dcName, ability)));\n        } else if (type == AppendTypeValue.abilityAttackMod) {\n            // {\n            //     \"type\": \"abilityAttackMod\",\n            //     \"name\": \"Spell\",\n            //     \"attributes\": [\n            //         \"cha\"\n            //     ]\n            // }\n            String attackName = SourceField.name.replaceTextFrom(entry, this);\n            text.add(spanWrap(\"abilityAttackMod\",\n                    getSources().isClassic()\n                            ? \"**%s attack modifier**: your proficiency bonus + your %s\"\n                                    .formatted(attackName, ability)\n                            : \"**%s attack modifier**: %s + Proficiency Bonus\"\n                                    .formatted(attackName, ability)));\n        } else { // abilityGeneric\n            List<String> inner = new ArrayList<>();\n            String name = SourceField.name.replaceTextFrom(entry, this);\n            if (isPresent(name)) {\n                inner.add(\"**\" + name + \".**\");\n            }\n            if (Tools5eFields.text.existsIn(entry)) {\n                Tools5eFields.text.replaceTextFrom(entry, this);\n            }\n            if (!abilities.isEmpty()) {\n                inner.add(ability);\n            }\n            text.add(spanWrap(\"abilityGeneric\", String.join(\" \", inner)));\n        }\n    }\n\n    default void appendAttack(List<String> text, JsonNode entry) {\n        String name = SourceField.name.replaceTextFrom(entry, this);\n        String attackType = AttackFields.attackType.getTextOrDefault(entry, \"MW\");\n        String atkString = flattenToString(AttackFields.attackEntries.getFrom(entry), \" \");\n        String hitString = flattenToString(AttackFields.hitEntries.getFrom(entry), \" \");\n\n        text.add(spanWrap(\"attack\",\n                \"%s*%s:* %s *Hit:* %s\".formatted(\n                        isPresent(name) ? \"***\" + name + \".*** \" : \"\",\n                        \"MW\".equals(attackType) ? \"Melee Weapon Attack\" : \"Ranged Weapon Attack\",\n                        atkString, hitString)));\n    }\n\n    default void appendCallout(String callout, String title, List<String> text, JsonNode entry) {\n        List<String> inner = new ArrayList<>();\n        appendToText(inner, SourceField.entries.getFrom(entry), null);\n\n        maybeAddBlankLine(text);\n        text.add(\"> [!\" + callout + \"] \" + replaceText(SourceField.name.getTextOrDefault(entry, title)));\n        inner.forEach(x -> text.add(\"> \" + x));\n    }\n\n    default void appendClassFeatureRef(List<String> text, JsonNode entry, Tools5eIndexType featureType, String fieldName) {\n        ClassFeature cf = Json2QuteClass.findClassFeature(this, featureType, entry, fieldName);\n        if (cf == null) {\n            return; // skipped or not found\n        }\n        if (parseState().featureTypeDepth() > 2) {\n            tui().errorf(\"Cycle in class or subclass features found in %s\", cf.cfSources());\n            // this is within an existing feature description. Emit as a link\n            cf.appendLink(this, text, parseState().getSource(featureType));\n        } else if (parseState().inList()) {\n            // emit within an existing list item\n            cf.appendListItemText(this, text, parseState().getSource(featureType));\n        } else {\n            // emit inline as proper section\n            cf.appendText(this, text, parseState().getSource(featureType));\n        }\n    }\n\n    default void appendEntriesToText(List<String> text, JsonNode entryNode, String heading) {\n        String name = SourceField.name.replaceTextFrom(entryNode, this);\n        if (heading == null) {\n            List<String> inner = new ArrayList<>();\n            appendToText(inner, SourceField.entries.getFrom(entryNode), null);\n            if (prependField(name, inner)) {\n                maybeAddBlankLine(text);\n            }\n            text.addAll(inner);\n        } else if (isPresent(name)) {\n            maybeAddBlankLine(text);\n            // strip links from heading titles. Cross-referencing headers with links is hard\n            text.add(heading + \" \" + name.replaceAll(\"\\\\[(.*?)\\\\]\\\\(.*?\\\\)\", \"$1\"));\n            if (!parseState().sourcePageString().isBlank() && index().differentSource(getSources(), parseState().getSource())) {\n                text.add(getSourceText(parseState()));\n            }\n            text.add(\"\");\n            appendToText(text, SourceField.entries.getFrom(entryNode), \"#\" + heading);\n        } else {\n            appendToText(text, SourceField.entries.getFrom(entryNode), heading);\n        }\n    }\n\n    /** Internal */\n    default void appendEntryItem(List<String> text, JsonNode itemNode) {\n        List<String> inner = new ArrayList<>();\n        appendToText(inner, SourceField.entry.getFrom(itemNode), null);\n        appendToText(inner, SourceField.entries.getFrom(itemNode), null);\n        if (SourceField.name.existsIn(itemNode) && prependField(itemNode, SourceField.name, inner)) {\n            maybeAddBlankLine(text);\n        }\n        text.addAll(inner);\n    }\n\n    default void appendGallery(List<String> text, JsonNode imageNode) {\n        text.add(\"> [!gallery]\");\n        imageNode.withArray(\"images\").forEach(image -> {\n            ImageRef imageRef = readImageRef(image);\n            if (imageRef != null) {\n                text.add(\"> \" + imageRef.getEmbeddedLink(\"gallery\"));\n            }\n        });\n    }\n\n    default void appendImage(List<String> text, JsonNode imageNode) {\n        ImageRef imageRef = readImageRef(imageNode);\n        if (imageRef == null) {\n            tui().warnf(\"Image information not found in %s\", imageNode);\n            return;\n        }\n        maybeAddBlankLine(text);\n        text.add(imageRef.getEmbeddedLink());\n    }\n\n    default void appendLink(List<String> text, JsonNode link) {\n        JsonMediaHref mediaRef = readLink(link);\n        if (mediaRef == null) {\n            tui().warnf(\"link information not found in %s\", link);\n            return;\n        }\n        if (\"external\".equals(mediaRef.href.type)) {\n            text.add(\"[\" + mediaRef.text + \"](\" + mediaRef.href.url + \")\");\n        }\n        text.add(mediaRef.text);\n    }\n\n    enum ListType {\n        unordered(\"- \"),\n        unstyled(\"\");\n\n        final String marker;\n\n        ListType(String marker) {\n            this.marker = marker;\n        }\n    }\n\n    default void appendList(List<String> text, ArrayNode itemArray, ListType listType) {\n        String indent = parseState().getListIndent();\n        boolean pushed = parseState().indentList();\n        try {\n            maybeAddBlankLine(text);\n            itemArray.forEach(e -> {\n                List<String> item = new ArrayList<>();\n                appendToText(item, e, null);\n                if (item.size() > 0) {\n                    text.add(indent + listType.marker + item.get(0) + \"  \");\n                    item.remove(0);\n                    item.forEach(x -> text.add(x.isEmpty() ? \"\" : indent + \"    \" + x + \"  \"));\n                }\n            });\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    default void appendOptionalFeature(List<String> text, JsonNode entry, String heading) {\n        maybeAddBlankLine(text);\n        text.add(heading + \" \" + SourceField.name.replaceTextFrom(entry, this));\n        String prereq = getTextOrDefault(entry, \"prerequisite\", null);\n        if (prereq != null) {\n            text.add(\"*Prerequisites* \" + prereq);\n        }\n        text.add(\"\");\n        appendToText(text, SourceField.entries.getFrom(entry), \"#\" + heading);\n    }\n\n    default void appendOptionalFeatureRef(List<String> text, JsonNode entry) {\n        String lookup = Tools5eFields.optionalfeature.getTextOrNull(entry);\n        if (lookup == null) {\n            tui().warnf(Msg.UNRESOLVED, \"Optional Feature not found in %s\", entry);\n            return; // skipped or not found\n        }\n        String[] parts = lookup.split(\"\\\\|\");\n        String nodeSource = parts.length > 1 && !parts[1].isBlank() ? parts[1]\n                : Tools5eIndexType.optfeature.defaultSourceString();\n        String key = Tools5eIndexType.optfeature.createKey(parts[0], nodeSource);\n        if (index().isIncluded(key)) {\n            if (parseState().inList()) {\n                text.add(linkify(Tools5eIndexType.optfeature, lookup));\n            } else {\n                tui().errorf(\"TODO refOptionalfeature %s -> %s\",\n                        lookup, Tools5eIndexType.optfeature.fromTagReference(lookup));\n            }\n        }\n    }\n\n    default void appendOptions(List<String> text, JsonNode entry) {\n        String indent = parseState().getListIndent();\n        boolean pushed = parseState().indentList();\n        try {\n            List<String> list = new ArrayList<>();\n            for (JsonNode e : iterableEntries(entry)) {\n                List<String> item = new ArrayList<>();\n                appendToText(item, e, null);\n                if (item.size() > 0) {\n                    StringBuilder listItem = new StringBuilder();\n                    listItem.append(indent).append(\"- \").append(item.get(0)).append(\"  \");\n                    item.remove(0);\n                    item.forEach(x -> listItem.append(x.isEmpty() ? \"\" : \"\\n\" + indent + \"    \" + x + \"  \"));\n                    list.add(listItem.toString());\n                }\n            }\n\n            if (list.size() > 0) {\n                maybeAddBlankLine(text);\n                int count = intOrDefault(entry, \"count\", 0);\n                text.add(String.format(\"Options%s:\",\n                        count > 0 ? \" (choose \" + count + \")\" : \"\"));\n                maybeAddBlankLine(text);\n                text.addAll(list);\n            }\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    default void appendInset(AppendTypeValue type, List<String> text, JsonNode entry) {\n        List<String> insetText = new ArrayList<>();\n        appendToText(insetText, SourceField.entries.getFrom(entry), null);\n        if (insetText.isEmpty()) {\n            return; // nothing to do (empty content)\n        }\n\n        String title = null;\n        String id = null;\n        if (entry.has(\"name\")) {\n            title = replaceText(SourceField.name.getTextOrEmpty(entry));\n            id = title;\n        } else if (getSources().getType() == Tools5eIndexType.race) {\n            title = insetText.remove(0);\n            id = title;\n        } else if (entry.has(\"id\")) {\n            id = SourceField.id.getTextOrEmpty(entry);\n        }\n\n        maybeAddBlankLine(text);\n        if (!insetText.isEmpty() && insetText.get(0).startsWith(\"> \")) {\n            // do not wrap empty or already inset content in another inset\n            text.addAll(insetText);\n        } else {\n            if (id != null) {\n                String admonition = type == AppendTypeValue.insetReadaloud ? \"[!readaloud] \" : \"[!note] \";\n                insetText.add(0, \"\");\n                insetText.add(0, admonition + (isPresent(title) ? title : \"\"));\n            }\n            insetText.forEach(x -> text.add(\"> \" + x));\n        }\n\n        if (isPresent(id)) {\n            text.add(\"^\" + slugify(id));\n        }\n    }\n\n    default void appendFlowchart(List<String> text, JsonNode entry, String heading) {\n        if (entry.has(\"name\")) {\n            maybeAddBlankLine(text);\n            text.add(heading + \" \" + SourceField.name.replaceTextFrom(entry, this));\n        }\n\n        for (JsonNode n : entry.withArray(\"blocks\")) {\n            maybeAddBlankLine(text);\n            text.add(\"> [!flowchart] \" + SourceField.name.replaceTextFrom(n, this));\n            for (JsonNode e : n.withArray(\"entries\")) {\n                text.add(\"> \" + replaceText(e.asText()));\n            }\n            text.add(\"%% %%\");\n        }\n    }\n\n    default void appendQuote(List<String> text, JsonNode entry) {\n        List<String> quoteText = new ArrayList<>();\n        if (entry.has(\"by\")) {\n            String by = replaceText(Tools5eFields.by.getTextOrEmpty(entry));\n            quoteText.add(\"[!quote] A quote from \" + by + \"  \");\n        } else {\n            quoteText.add(\"[!quote]  \");\n        }\n        appendToText(quoteText, SourceField.entries.getFrom(entry), null);\n\n        maybeAddBlankLine(text);\n        quoteText.forEach(x -> text.add(\"> \" + x));\n        maybeAddBlankLine(text);\n    }\n\n    default void appendStatblock(List<String> text, JsonNode entry, String heading) {\n        // Most use \"tag\", except for subclass, which uses \"prop\"\n        String tagPropText = Tools5eFields.tag.getTextOrDefault(entry, Tools5eFields.prop.getTextOrEmpty(entry));\n        Tools5eIndexType type = Tools5eIndexType.fromText(tagPropText);\n        if (type == null) {\n            tui().debugf(Msg.SOMEDAY, \"Unrecognized statblock type in %s\", entry);\n            return;\n        }\n        embedReference(text, entry, type, heading);\n    }\n\n    default void appendStatblockInline(List<String> text, JsonNode entry, String heading) {\n        // For inline statblocks, we start with the dataType\n        Tools5eIndexType type = Tools5eIndexType.fromText(Tools5eFields.dataType.getTextOrEmpty(entry));\n        if (type == null) {\n            tui().debugf(Msg.SOMEDAY, \"Unrecognized statblock dataType in %s\", entry);\n            return;\n        }\n        JsonNode data = Tools5eFields.data.getFrom(entry);\n        if (data == null) {\n            tui().errorf(\"No data found in %s\", entry);\n            return;\n        }\n        // Replace text in embedded data node w/ trimmed name (ensure keys match)\n        String name = SourceField.name.replaceTextFrom(data, this);\n        ((ObjectNode) data).set(\"name\", new TextNode(name));\n\n        String source = SourceField.source.getTextOrEmpty(data);\n        String finalKey = type.createKey(data);\n\n        JsonNode existingNode = index().getNode(finalKey);\n        TtrpgValue.indexKey.setIn(data, finalKey);\n        TtrpgValue.indexInputType.setIn(data, type.name());\n\n        // TODO: Remove me.\n        JsonNode copy = MetaFields._copy.getFrom(data);\n        if (copy != null) {\n            String copyName = SourceField.name.getTextOrEmpty(copy).strip();\n            String copySource = SourceField.source.getTextOrEmpty(copy).strip();\n            if (name.equals(copyName) && source.equals(copySource)) {\n                embedReference(text, data, type, heading); // embed note that will be present in the final output\n                return;\n            }\n            Tools5eJsonSourceCopier copier = new Tools5eJsonSourceCopier(index());\n            data = copier.handleCopy(type, data);\n            existingNode = null; // this is a modified node, ignore existing.\n        } else if (equivalentNode(data, existingNode) && index().isIncluded(finalKey)) {\n            embedReference(text, data, type, heading); // embed note that will be present in the final output\n            return;\n        } else if (existingNode == null) {\n            Tools5eSources.constructSources(finalKey, data);\n        }\n\n        Tools5eQuteBase qs = null;\n        switch (type) {\n            case item -> qs = new Json2QuteItem(index(), type, data).build();\n            case monster -> qs = new Json2QuteMonster(index(), type, data).build();\n            case object -> qs = new Json2QuteObject(index(), type, data).build();\n            case spell -> qs = new Json2QuteSpell(index(), type, data).build();\n            default -> tui().errorf(\"Not ready for statblock dataType in %s\", entry);\n        }\n        if (qs != null) {\n            if (type == Tools5eIndexType.monster) {\n                // Create a new monster document (header for initiative tracker)\n                String embedFileName = Tui.slugify(String.format(\"%s-%s-%s\", getSources().getName(), type.name(), name));\n                String relativePath = linkifier().getRelativePath(getSources());\n                String vaultRoot = linkifier().vaultRoot(getSources());\n\n                maybeAddBlankLine(text);\n                text.add(\"> [!embed-monster]- \" + name);\n                text.add(String.format(\"> ![%s](%s%s/%s#^statblock)\", name, vaultRoot, relativePath, embedFileName));\n\n                // remember the file that should be created later.\n                // use the relative path for the containing note (not the bestiary)\n                getSources().addInlineNote(qs\n                        .withTargetFile(embedFileName)\n                        .withTargetPath(relativePath));\n            } else {\n                List<String> prepend = new ArrayList<>(List.of(\n                        \"title: \" + name,\n                        \"collapse: closed\",\n                        existingNode == null ? \"\" : \"%% See \" + type.linkify(this, data) + \" %%\"));\n                renderEmbeddedTemplate(text, qs, type.name(), prepend);\n            }\n        }\n    }\n\n    default boolean equivalentNode(JsonNode dataNode, JsonNode existingNode) {\n        if (existingNode == null || dataNode == null || dataNode.has(\"_copy\")) {\n            return false;\n        }\n        for (Entry<String, JsonNode> field : iterableFields(dataNode)) {\n            JsonNode existingField = existingNode.get(field.getKey());\n            if (existingField == null || !field.getValue().equals(existingField)) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    default void embedReference(List<String> text, JsonNode entry, Tools5eIndexType type, String heading) {\n        String name = SourceField.name.getTextOrEmpty(entry);\n        String key = index().getAliasOrDefault(type.createKey(entry));\n        Tools5eSources sources = Tools5eSources.findSources(key);\n        if (key == null || sources == null) {\n            tui().debugf(Msg.UNKNOWN, \"unable to find statblock target %s from %s in %s\", key, entry, getSources());\n            return;\n        }\n\n        if (type == Tools5eIndexType.charoption) {\n            // charoption is not a linkable type.\n            tui().debugf(Msg.SOMEDAY, \"charoption is not yet an embeddable type: %s\", entry);\n            return;\n        } else if (type.isFluffType()) {\n            // Fluff is not a linkable type, and is never added to the filtered index,\n            // so we need to check if the material is included in other ways\n            JsonNode fluffNode = index().getOrigin(key);\n            if (!sources.includedByConfig() || fluffNode == null) {\n                // do nothing if the source isn't included\n                return;\n            }\n            List<ImageRef> images = new ArrayList<>();\n            unpackFluffNode(type, fluffNode, text, null, images);\n            maybeAddBlankLine(text);\n            return;\n        } else if (type == Tools5eIndexType.reference) {\n            // reference is not a linkable type.\n            tui().debugf(Msg.SOMEDAY, \"reference is not yet an embeddable type: %s\", entry);\n            return;\n        }\n\n        String link = type.linkify(this, entry);\n\n        if (link.matches(\"\\\\[.*]\\\\(.*\\\\)\")) {\n            maybeAddBlankLine(text);\n            text.add(\"> [!embed-\" + type.name() + \"]- \" + name);\n            if (type == Tools5eIndexType.monster) {\n                text.add(\"> !\" + link.replaceAll(\"\\\\)$\", \"#^statblock)\"));\n            } else {\n                text.add(\"> !\" + link);\n            }\n        } else {\n            text.add(link);\n        }\n        // 🔸 WARN| 🫣 unable to find statblock target {\"type\":\"statblock\",\"prop\":\"subclass\",\"source\":\"XUA2023PlayersHandbookP7\",\"name\":\"Aberrant\",\"className\":\"Sorcerer\",\"classSource\":\"XUA2023PlayersHandbookP7\",\"collapsed\":true,\"displayName\":\"Aberrant Sorcery\",\"indexInputType\":\"reference\",\"indexKey\":\"reference|aberrant|xua2023playershandbookp7\"} from sources[book|book-xua2023playershandbookp7]\n        // 🔸 WARN| 🫣 unable to find statblock target {\"type\":\"statblock\",\"prop\":\"subclass\",\"source\":\"XUA2023PlayersHandbookP7\",\"name\":\"Clockwork\",\"className\":\"Sorcerer\",\"classSource\":\"XUA2023PlayersHandbookP7\",\"collapsed\":true,\"displayName\":\"Clockwork Sorcery\",\"indexInputType\":\"reference\",\"indexKey\":\"reference|clockwork|xua2023playershandbookp7\"} from sources[book|book-xua2023playershandbookp7]\n        // 🔸 WARN| 🫣 unable to find statblock target {\"type\":\"statblock\",\"tag\":\"variantrule\",\"source\":\"ESK\",\"name\":\"Sidekicks\",\"page\":66,\"indexInputType\":\"reference\",\"indexKey\":\"reference|sidekicks|esk\"} from sources[adventure|adventure-dip]\n    }\n\n    default void appendTable(List<String> text, JsonNode tableNode) {\n        boolean pushed = parseState().push(tableNode);\n        try {\n            List<String> table = new ArrayList<>();\n\n            String caption = TableFields.caption.getTextOrEmpty(tableNode);\n            String blockid = caption.isBlank()\n                    ? \"\"\n                    : \"^\" + slugify(caption);\n\n            String name = findTableName(tableNode);\n            String known = findTable(Tools5eIndexType.table, tableNode);\n            if (known != null) {\n                appendTableText(name, List.of(known), text, tableNode);\n                return;\n            }\n\n            // We could be working on a table, or a table nested inside a table.\n            // Gather text in \"inner\", then we'll decide how to append it later.\n            List<String> inner = new ArrayList<>();\n\n            if (TableFields.colLabelRows.existsIn(tableNode)) {\n                name = appendHtmlTable(table, inner, tableNode, caption, blockid, name);\n            } else {\n                name = appendMarkdownTable(table, inner, tableNode, caption, blockid, name);\n            }\n\n            // Add directly to text, or stick in a tableNode for later.\n            appendTableText(name, inner, text, tableNode);\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    default String appendMarkdownTable(List<String> table, List<String> inner,\n            JsonNode tableNode, String caption, String blockid, String name) {\n        boolean pushTable = parseState().pushMarkdownTable(true);\n        try {\n            String header;\n            if (TableFields.colLabels.existsIn(tableNode)) {\n                List<String> labels = TableFields.colLabels.getListOfStrings(tableNode, tui());\n                header = String.join(\" | \", labels.stream()\n                        .map(x -> tableHeader(x))\n                        .toList());\n\n                if (blockid.isEmpty()) {\n                    blockid = \"^\" + slugify(header\n                            .replaceAll(\"dice: \", \"\")\n                            .replaceAll(\"d\\\\d+\", \"\")\n                            .replaceAll(\"</?span.*?>\", \"\")\n                            .replace(\"|\", \"\")\n                            .replaceAll(\"\\\\s+\", \" \")\n                            .trim());\n                }\n            } else if (TableFields.colStyles.existsIn(tableNode)) {\n                header = TableFields.colStyles.getListOfStrings(tableNode, tui()).stream()\n                        .map(x -> \"  \")\n                        .collect(Collectors.joining(\" | \"));\n            } else {\n                int length = TableFields.rows.size(tableNode);\n                String[] array = new String[length];\n                Arrays.fill(array, \" \");\n                header = \"|\" + String.join(\" | \", array) + \" |\";\n            }\n\n            final boolean cards = header.contains(\"Card | \");\n            for (JsonNode r : TableFields.rows.iterateArrayFrom(tableNode)) {\n                JsonNode cells;\n                boolean indentFirst = false;\n                if (\"row\".equals(TableFields.type.getTextOrNull(r))) {\n                    cells = TableFields.row.getFrom(r);\n                    indentFirst = TableFields.style.getTextOrEmpty(r).equals(\"row-indent-first\");\n                } else {\n                    cells = r;\n                }\n\n                String row = indentFirst\n                        ? \"| &emsp;\"\n                        : \"| \";\n                row += streamOf(cells)\n                        .map(x -> {\n                            JsonNode roll = RollFields.roll.getFrom(x);\n                            if (roll != null) {\n                                String result = \"\";\n                                if (RollFields.exact.existsIn(roll)) {\n                                    result = RollFields.exact.getFrom(roll).asText();\n                                } else {\n                                    result = RollFields.min.getTextOrEmpty(roll) + \"-\"\n                                            + RollFields.max.getTextOrEmpty(roll);\n                                }\n                                if (cards) {\n                                    result += \" | \" + SourceField.entry.getTextOrEmpty(x);\n                                }\n                                return new TextNode(result);\n                            }\n                            return x;\n                        })\n                        .map(x -> {\n                            String s = flattenToString(x).replace(\"\\n\", \"<br />\");\n                            JsonNode nestedTable = TableFields.ttrpgNestedTable.getFrom(x);\n                            if (nestedTable != null) {\n                                TableFields.ttrpgNestedTable.appendToArray(tableNode, nestedTable);\n                            }\n                            return s;\n                        })\n                        .collect(Collectors.joining(\" | \")) + \" |\";\n\n                table.add(row);\n            }\n\n            header = \"| \" + header + \" |\";\n            table.add(0, header.replaceAll(\"[^|]\", \"-\"));\n            table.add(0, header);\n\n            if (!blockid.isBlank()) {\n                table.add(blockid);\n            }\n            if (cfg().useDiceRoller().enabled()\n                    && header.matches(JsonTextConverter.DICE_TABLE_HEADER)\n                    && !blockid.isBlank()) {\n                // prepend a dice roller\n                String targetFile = getFileName();\n                // use dice roller string as name (for use if nested table)\n                name = String.format(\"`dice: [](%s.md#%s)`\", targetFile, blockid);\n                table.add(0, name);\n                table.add(1, \"\");\n            }\n            if (!caption.isBlank()) {\n                table.add(0, \"\");\n                table.add(0, \"**\" + replaceText(caption) + \"**\");\n            }\n\n            switch (blockid) {\n                case \"personality-trait\" -> Json2QuteBackground.traits.addAll(table);\n                case \"ideal\" -> Json2QuteBackground.ideals.addAll(table);\n                case \"bond\" -> Json2QuteBackground.bonds.addAll(table);\n                case \"flaw\" -> Json2QuteBackground.flaws.addAll(table);\n            }\n\n            JsonNode intro = TableFields.intro.getFrom(tableNode);\n            if (intro != null) {\n                maybeAddBlankLine(inner);\n                appendToText(inner, intro, null);\n            }\n            maybeAddBlankLine(inner);\n            inner.addAll(table);\n\n            JsonNode footnotes = TableFields.footnotes.getFrom(tableNode);\n            if (footnotes != null) {\n                maybeAddBlankLine(inner);\n                boolean pushF = parseState().pushFootnotes(true);\n                appendToText(inner, footnotes, null);\n                parseState().pop(pushF);\n            }\n            JsonNode outro = TableFields.outro.getFrom(tableNode);\n            if (outro != null) {\n                maybeAddBlankLine(inner);\n                appendToText(inner, outro, null);\n            }\n        } finally {\n            parseState().pop(pushTable);\n        }\n        return name;\n    }\n\n    default String appendHtmlTable(List<String> table, List<String> inner,\n            JsonNode tableNode, String caption, String blockid, String name) {\n        boolean pushTable = parseState().pushHtmlTable(true);\n        try {\n            if (!caption.isBlank()) {\n                inner.add(\"\");\n                inner.add(\"**\" + replaceText(caption) + \"**\");\n            }\n\n            JsonNode intro = TableFields.intro.getFrom(tableNode);\n            if (intro != null) {\n                maybeAddBlankLine(inner);\n                appendToText(inner, intro, null);\n            }\n\n            maybeAddBlankLine(inner);\n            table.add(\"<table>\");\n\n            // Header rows from colLabelRows\n            for (JsonNode headerRow : TableFields.colLabelRows.iterateArrayFrom(tableNode)) {\n                table.add(\"<tr>\");\n                for (JsonNode cell : iterableElements(headerRow)) {\n                    if (cell.isObject() && \"cellHeader\".equals(cell.path(\"type\").asText())) {\n                        int width = cell.path(\"width\").asInt(1);\n                        String cellText = markdownLinkToHtml(replaceText(cell.path(\"entry\").asText(\"\")));\n                        table.add(String.format(\"  <th colspan=\\\"%d\\\">%s</th>\", width, cellText));\n                    } else {\n                        String cellText = cell.isTextual()\n                                ? markdownLinkToHtml(replaceText(cell.asText()))\n                                : markdownLinkToHtml(flattenToString(cell));\n                        table.add(\"  <th>\" + cellText + \"</th>\");\n                    }\n                }\n                table.add(\"</tr>\");\n            }\n\n            // Data rows\n            for (JsonNode r : TableFields.rows.iterateArrayFrom(tableNode)) {\n                JsonNode cells;\n                boolean indentFirst = false;\n                if (\"row\".equals(TableFields.type.getTextOrNull(r))) {\n                    cells = TableFields.row.getFrom(r);\n                    indentFirst = TableFields.style.getTextOrEmpty(r).equals(\"row-indent-first\");\n                } else {\n                    cells = r;\n                }\n\n                table.add(\"<tr>\");\n                boolean first = true;\n                for (JsonNode x : iterableElements(cells)) {\n                    JsonNode roll = RollFields.roll.getFrom(x);\n                    String cellText;\n                    if (roll != null) {\n                        if (RollFields.exact.existsIn(roll)) {\n                            cellText = RollFields.exact.getFrom(roll).asText();\n                        } else {\n                            cellText = RollFields.min.getTextOrEmpty(roll) + \"-\"\n                                    + RollFields.max.getTextOrEmpty(roll);\n                        }\n                    } else {\n                        cellText = markdownLinkToHtml(flattenToString(x).replace(\"\\n\", \"<br />\"));\n                    }\n                    JsonNode nestedTable = TableFields.ttrpgNestedTable.getFrom(x);\n                    if (nestedTable != null) {\n                        TableFields.ttrpgNestedTable.appendToArray(tableNode, nestedTable);\n                    }\n                    if (first && indentFirst) {\n                        cellText = \"&emsp;\" + cellText;\n                    }\n                    table.add(\"  <td>\" + cellText + \"</td>\");\n                    first = false;\n                }\n                table.add(\"</tr>\");\n            }\n\n            table.add(\"</table>\");\n\n            // Derive blockid from last header row if not set from caption\n            if (blockid.isEmpty()) {\n                JsonNode labelRows = TableFields.colLabelRows.getFrom(tableNode);\n                if (labelRows != null && labelRows.size() > 0) {\n                    JsonNode lastRow = labelRows.get(labelRows.size() - 1);\n                    String headerText = streamOf(lastRow)\n                            .map(x -> x.isObject() ? x.path(\"entry\").asText(\"\") : x.asText())\n                            .collect(Collectors.joining(\" \"));\n                    blockid = \"^\" + slugify(headerText.trim());\n                }\n            }\n            if (!blockid.isBlank()) {\n                table.add(blockid);\n            }\n\n            inner.addAll(table);\n\n            JsonNode footnotes = TableFields.footnotes.getFrom(tableNode);\n            if (footnotes != null) {\n                maybeAddBlankLine(inner);\n                boolean pushF = parseState().pushFootnotes(true);\n                appendToText(inner, footnotes, null);\n                parseState().pop(pushF);\n            }\n            JsonNode outro = TableFields.outro.getFrom(tableNode);\n            if (outro != null) {\n                maybeAddBlankLine(inner);\n                appendToText(inner, outro, null);\n            }\n        } finally {\n            parseState().pop(pushTable);\n        }\n        return name;\n    }\n\n    default void appendTableText(String name, List<String> inner, List<String> text, JsonNode tableNode) {\n        if (parseState().inTable()) {\n            // we are inside a table row. Append this text to an element in the tableNode\n            // that will be rendered after the table.\n            TableFields.ttrpgNestedTable.appendToArray(tableNode, String.join(\"\\n\", inner));\n            text.add(name);\n        } else {\n            maybeAddBlankLine(text);\n            text.addAll(inner);\n            if (TableFields.ttrpgNestedTable.existsIn(tableNode)) {\n                // This table had nested tables! Append them, too (already formatted)\n                for (JsonNode nested : TableFields.ttrpgNestedTable.iterateArrayFrom(tableNode)) {\n                    maybeAddBlankLine(text);\n                    text.add(nested.asText());\n                }\n            }\n        }\n    }\n\n    default void appendTableGroup(List<String> text, JsonNode tableGroup, String heading) {\n        boolean pushed = parseState().push(tableGroup);\n        try {\n            String name = findTableName(tableGroup);\n            String known = findTable(Tools5eIndexType.tableGroup, tableGroup);\n            if (known != null) {\n                maybeAddBlankLine(text);\n                text.add(known);\n                return;\n            }\n\n            maybeAddBlankLine(text);\n            text.add(heading + \" \" + name);\n            if (index().differentSource(getSources(), parseState().getSource())) {\n                text.add(getSourceText(parseState()));\n            }\n            maybeAddBlankLine(text);\n            appendToText(text, Tools5eFields.tables.getFrom(tableGroup), \"#\" + heading);\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    default String findTableName(JsonNode tableNode) {\n        return TableFields.caption.getTextOrDefault(tableNode,\n                SourceField.name.getTextOrEmpty(tableNode));\n    }\n\n    default String findTable(Tools5eIndexType keyType, JsonNode matchTable) {\n        if (getSources().getType() == Tools5eIndexType.table || getSources().getType() == Tools5eIndexType.tableGroup) {\n            return null;\n        }\n        String name = findTableName(matchTable);\n        String tableKey = keyType.createKey(name, parseState().getSource());\n        JsonNode knownEntry = index().getNode(tableKey);\n        if (knownEntry == null && keyType == Tools5eIndexType.table) {\n            SourceAndPage sp = parseState().toSourceAndPage();\n            knownEntry = index().findTable(sp, TableFields.getFirstRow(matchTable));\n        }\n        if (knownEntry != null) {\n            String link = keyType.linkify(this, knownEntry);\n            return link.matches(\"\\\\[.+]\\\\(.+\\\\)\") ? \"!\" + link : null;\n        }\n        return null;\n    }\n\n    default void unpackFluffNode(Tools5eIndexType fluffType, JsonNode fluffNode, List<String> text, String heading,\n            List<ImageRef> images) {\n\n        boolean pushed = parseState().push(getSources(), fluffNode);\n        try {\n            if (fluffNode.isArray()) {\n                appendToText(text, fluffNode, heading);\n            } else {\n                appendToText(text, SourceField.entries.getFrom(fluffNode), heading);\n            }\n        } finally {\n            parseState().pop(pushed);\n        }\n\n        if (Tools5eFields.images.existsIn(fluffNode)) {\n            getImages(Tools5eFields.images.getFrom(fluffNode), images);\n        } else if (Tools5eFields.hasFluffImages.booleanOrDefault(fluffNode, false)) {\n            String fluffKey = fluffType.createKey(fluffNode);\n            fluffNode = index().getOrigin(fluffKey);\n            if (fluffNode != null) {\n                getImages(Tools5eFields.images.getFrom(fluffNode), images);\n            }\n        }\n    }\n\n    default void getImages(JsonNode imageNode, List<ImageRef> images) {\n        if (imageNode != null && imageNode.isArray()) {\n            for (Iterator<JsonNode> i = imageNode.elements(); i.hasNext();) {\n                ImageRef ir = readImageRef(i.next());\n                if (ir != null) {\n                    images.add(ir);\n                }\n            }\n        }\n    }\n\n    default ImageRef readImageRef(JsonNode imageNode) {\n        try {\n            JsonMediaHref mediaHref = mapper().treeToValue(imageNode, JsonMediaHref.class);\n            return buildImageRef(mediaHref, getImagePath());\n        } catch (JsonProcessingException | IllegalArgumentException e) {\n            tui().errorf(e, \"Unable to read media reference from %s: %s\", imageNode, e.toString());\n        }\n        return null;\n    }\n\n    default JsonMediaHref readLink(JsonNode linkNode) {\n        try {\n            return mapper().treeToValue(linkNode, JsonMediaHref.class);\n        } catch (JsonProcessingException | IllegalArgumentException e) {\n            tui().errorf(e, \"Unable to read link from %s: %s\", linkNode, e.toString());\n        }\n        return null;\n    }\n\n    default JsonHref readHref(JsonNode href) {\n        try {\n            return mapper().treeToValue(href, JsonHref.class);\n        } catch (JsonProcessingException | IllegalArgumentException e) {\n            tui().errorf(e, \"Unable to read href from %s: %s\", href, e.toString());\n        }\n        return null;\n    }\n\n    default String asAbilityEnum(JsonNode textNode) {\n        return SkillOrAbility.format(textNode.asText(), index(), getSources());\n    }\n\n    default String toAlignmentCharacters(String src) {\n        return src.replaceAll(\"\\\"[A-Z]*[a-z ]+\\\"\", \"\") // remove notes\n                .replaceAll(\"[^LCNEGAUXY]\", \"\"); // keep only alignment characters\n    }\n\n    default String alignmentListToFull(JsonNode alignmentList) {\n        if (alignmentList == null) {\n            return \"\";\n        }\n        boolean allText = streamOf(alignmentList).allMatch(JsonNode::isTextual);\n        boolean allObject = streamOf(alignmentList).allMatch(JsonNode::isObject);\n\n        if (allText) {\n            return mapAlignmentToString(toAlignmentCharacters(alignmentList.toString()));\n        } else if (allObject) {\n            return streamOf(alignmentList)\n                    .filter(x -> AlignmentFields.alignment.existsIn(x))\n                    .map(x -> {\n                        if (AlignmentFields.special.existsIn(x)\n                                || AlignmentFields.chance.existsIn(x)\n                                || AlignmentFields.note.existsIn(x)) {\n                            return alignmentObjToFull(x);\n                        } else {\n                            return alignmentListToFull(x.get(\"alignment\"));\n                        }\n                    })\n                    .collect(Collectors.joining(\" or \"));\n\n        } else {\n            tui().errorf(\"Unable to parse alignment list from %s\", alignmentList);\n        }\n        return \"\";\n    }\n\n    default String alignmentObjToFull(JsonNode alignmentNode) {\n        if (alignmentNode == null) {\n            return null;\n        }\n        if (alignmentNode.isObject()) {\n            if (AlignmentFields.special.existsIn(alignmentNode)) {\n                return AlignmentFields.special.replaceTextFrom(alignmentNode, index());\n            } else {\n                String chance = \"\";\n                String note = \"\";\n                if (AlignmentFields.chance.existsIn(alignmentNode)) {\n                    chance = String.format(\" (%s%%)\", AlignmentFields.chance.getFrom(alignmentNode));\n                }\n                if (AlignmentFields.note.existsIn(alignmentNode)) {\n                    note = \" (\" + AlignmentFields.note.replaceTextFrom(alignmentNode, index()) + \")\";\n                }\n                return String.format(\"%s%s%s\",\n                        alignmentObjToFull(AlignmentFields.alignment.getFrom(alignmentNode)),\n                        chance, note);\n            }\n        }\n        return mapAlignmentToString(alignmentNode.asText().toUpperCase());\n    }\n\n    default String mapAlignmentToString(String a) {\n        return switch (a.toUpperCase()) {\n            case \"A\" -> \"Any alignment\";\n            case \"C\" -> \"Chaotic\";\n            case \"CE\" -> \"Chaotic Evil\";\n            case \"CG\" -> \"Chaotic Good\";\n            case \"CECG\", \"CGCE\" -> \"Chaotic Evil or Chaotic Good\";\n            case \"CGCN\" -> \"Chaotic Good or Chaotic Neutral\";\n            case \"CGNE\" -> \"Chaotic Good or Neutral Evil\";\n            case \"CECN\" -> \"Chaotic Evil or Chaotic Neutral\";\n            case \"CGNYE\" -> \"Any Chaotic alignment\";\n            case \"CN\" -> \"Chaotic Neutral\";\n            case \"CENE\", \"NECE\" -> \"Chaotic Evil or Neutral Evil\";\n            case \"L\" -> \"Lawful\";\n            case \"LE\" -> \"Lawful Evil\";\n            case \"LG\" -> \"Lawful Good\";\n            case \"LN\" -> \"Lawful Neutral\";\n            case \"LNCE\" -> \"Lawful Neutral or Chaotic Evil\";\n            case \"LELG\" -> \"Lawful Evil or Lawful Good\";\n            case \"LELN\", \"LNLE\" -> \"Lawful Evil or Lawful Neutral\";\n            case \"N\", \"NXNY\", \"NXNYN\", \"NNXNYN\" -> \"Neutral\";\n            case \"NX\" -> \"Neutral (law/chaos axis)\";\n            case \"NY\" -> \"Neutral (good/evil axis)\";\n            case \"NE\" -> \"Neutral Evil\";\n            case \"NG\" -> \"Neutral Good\";\n            case \"NGNE\", \"NENG\" -> \"Neutral Good or Neutral Evil\";\n            case \"G\", \"LNXCG\" -> \"Any Good alignment\";\n            case \"E\", \"CELENE\", \"LNXCE\" -> \"Any Evil alignment\";\n            case \"NELE\", \"LENE\" -> \"Neutral Evil or Lawful Evil\";\n            case \"LGNYE\" -> \"Any Non-Chaotic alignment\";\n            case \"LNXCNYE\" -> \"Any Non-Good alignment\";\n            case \"NXCGNYE\" -> \"Any Non-Lawful alignment\";\n            case \"NXLGNYE\" -> \"Any Non-Chaotic alignment\";\n            case \"LNXCNYG\", \"LNYNXCG\" -> \"Any Non-Evil alignment\";\n            case \"U\" -> \"Unaligned\";\n            default -> {\n                tui().errorf(\"What alignment is this? %s (from %s)\", a, getSources());\n                yield \"Unknown\";\n            }\n        };\n    }\n\n    static int levelToPb(int level) {\n        // 2 + (¼ * (Level – 1))\n        return 2 + ((int) (.25 * (level - 1)));\n    }\n\n    default String monsterCr(JsonNode monster) {\n        if (monster.has(\"cr\")) {\n            JsonNode crNode = Tools5eFields.cr.getFrom(monster);\n            if (crNode.isTextual()) {\n                return crNode.asText();\n            } else if (crNode.has(\"cr\")) {\n                return Tools5eFields.cr.getFrom(crNode).asText();\n            } else {\n                tui().errorf(\"Unable to parse cr value from %s\", crNode.toPrettyString());\n            }\n        }\n        return null;\n    }\n\n    default double crToXp(JsonNode cr) {\n        if (Tools5eFields.xp.existsIn(cr)) {\n            return Tools5eFields.xp.getFrom(cr).asDouble();\n        }\n        if (Tools5eFields.cr.existsIn(cr)) {\n            cr = Tools5eFields.cr.getFrom(cr);\n        }\n        return XP_CHART_ALT.get(cr.asText());\n    }\n\n    default int crToPb(JsonNode cr) {\n        if (cr.isTextual()) {\n            return crToPb(cr.asText());\n        }\n        return crToPb(Tools5eFields.cr.getFrom(cr).asText());\n    }\n\n    default int crToPb(String crValue) {\n        double crDouble = crToNumber(crValue);\n        if (crDouble < 5)\n            return 2;\n        return (int) Math.ceil(crDouble / 4) + 1;\n    }\n\n    default double crToNumber(String crValue) {\n        if (crValue == null || crValue.equals(\"Unknown\") || crValue.equals(\"\\u2014\")) {\n            return CR_UNKNOWN;\n        }\n        String[] parts = crValue.trim().split(\"/\");\n        try {\n            if (parts.length == 1) {\n                return Double.parseDouble(parts[0]);\n            } else if (parts.length == 2) {\n                return Double.parseDouble(parts[0]) / Double.parseDouble(parts[1]);\n            }\n        } catch (NumberFormatException nfe) {\n            return CR_CUSTOM;\n        }\n        return 0;\n    }\n\n    default String getSize(JsonNode value) {\n        JsonNode size = Tools5eFields.size.getFrom(value);\n        if (size != null) {\n            try {\n                if (size.isTextual()) {\n                    return sizeToString(size.asText());\n                } else if (size.isArray()) {\n                    String merged = streamOf(size).map(JsonNode::asText).collect(Collectors.joining());\n                    return sizeToString(merged);\n                }\n            } catch (IllegalArgumentException ignored) {\n            }\n        }\n        tui().errorf(\"Unable to parse size for %s from %s\", getSources(), size);\n        return \"Unknown\";\n    }\n\n    default String spanWrap(String cssClass, String text) {\n        return parseState().inTrait()\n                ? text\n                : \"<span class='%s'>%s</span>\".formatted(cssClass, text);\n    }\n\n    default String sizeToString(String size) {\n        return switch (size) {\n            case \"F\" -> \"Fine\";\n            case \"D\" -> \"Diminutive\";\n            case \"T\" -> \"Tiny\";\n            case \"S\" -> \"Small\";\n            case \"M\" -> \"Medium\";\n            case \"L\" -> \"Large\";\n            case \"H\" -> \"Huge\";\n            case \"G\" -> \"Gargantuan\";\n            case \"C\" -> \"Colossal\";\n            case \"V\" -> \"Varies\";\n            case \"SM\" -> \"Small or Medium\";\n            default -> \"Unknown\";\n        };\n    }\n\n    static String crToTagValue(String cr) {\n        return switch (cr) {\n            case \"1/8\" -> \"⅛\";\n            case \"1/4\" -> \"¼\";\n            case \"1/2\" -> \"½\";\n            default -> cr;\n        };\n    }\n\n    default String asModifier(double value) {\n        return (value >= 0 ? \"+\" : \"\") + value;\n    }\n\n    default String asModifier(int value) {\n        return (value >= 0 ? \"+\" : \"\") + value;\n    }\n\n    default String articleFor(String value) {\n        value = leadingNumber.matcher(value).replaceAll((m) -> {\n            return numberToText(Integer.parseInt(m.group(1))) + m.group(2);\n        });\n\n        return switch (value.toLowerCase().charAt(0)) {\n            case 'a', 'e', 'i', 'o', 'u' -> \"an\";\n            default -> \"a\";\n        };\n    }\n\n    default String numberToText(int value) {\n        int abs = Math.abs(value);\n        if (abs >= 100) {\n            return \"\" + value;\n        }\n        String strValue = switch (abs) {\n            case 0 -> \"zero\";\n            case 1 -> \"one\";\n            case 2 -> \"two\";\n            case 3 -> \"three\";\n            case 4 -> \"four\";\n            case 5 -> \"five\";\n            case 6 -> \"six\";\n            case 7 -> \"seven\";\n            case 8 -> \"eight\";\n            case 9 -> \"nine\";\n            case 10 -> \"ten\";\n            case 11 -> \"eleven\";\n            case 12 -> \"twelve\";\n            case 13 -> \"thirteen\";\n            case 14 -> \"fourteen\";\n            case 15 -> \"fifteen\";\n            case 16 -> \"sixteen\";\n            case 17 -> \"seventeen\";\n            case 18 -> \"eighteen\";\n            case 19 -> \"nineteen\";\n            case 20 -> \"twenty\";\n            case 30 -> \"thirty\";\n            case 40 -> \"forty\";\n            case 50 -> \"fifty\";\n            case 60 -> \"sixty\";\n            case 70 -> \"seventy\";\n            case 80 -> \"eighty\";\n            case 90 -> \"ninety\";\n            default -> {\n                yield null;\n            }\n        };\n\n        return strValue == null\n                ? String.valueOf(value)\n                : String.format(\"%s%s\",\n                        value < 0 ? \"negative \" : \"\",\n                        strValue);\n    }\n\n    default String damageTypeToFull(String dmgType) {\n        if (!isPresent(dmgType)) {\n            return \"\";\n        }\n        return switch (dmgType.toUpperCase()) {\n            case \"A\" -> \"acid\";\n            case \"B\" -> \"bludgeoning\";\n            case \"C\" -> \"cold\";\n            case \"F\" -> \"fire\";\n            case \"O\" -> \"force\";\n            case \"L\" -> \"lightning\";\n            case \"N\" -> \"necrotic\";\n            case \"P\" -> \"piercing\";\n            case \"I\" -> \"poison\";\n            case \"Y\" -> \"psychic\";\n            case \"R\" -> \"radiant\";\n            case \"S\" -> \"slashing\";\n            case \"T\" -> \"thunder\";\n            default -> dmgType;\n        };\n    };\n\n    default String convertCurrency(int cp) {\n        List<String> result = new ArrayList<>();\n        int gp = cp / 100;\n        cp %= 100;\n        if (gp > 0) {\n            result.add(String.format(\"%,d gp\", gp));\n        }\n        int sp = cp / 10;\n        cp %= 10;\n        if (sp > 0) {\n            result.add(String.format(\"%,d sp\", sp));\n        }\n        if (cp > 0) {\n            result.add(String.format(\"%,d cp\", cp));\n        }\n        return String.join(\", \", result);\n    }\n\n    public static String featureTypeToString(String featureType, Map<String, HomebrewMetaTypes> homebrew) {\n        if (!isPresent(featureType)) {\n            return \"\";\n        }\n\n        // Parser.FEAT_CATEGORY_TO_FULL\n        // Parser.OPT_FEATURE_TYPE_TO_FULL\n        return switch (featureType.toUpperCase()) {\n            case \"D\" -> \"Dragonmark\";\n            case \"EB\" -> \"Epic Boon Feat\";\n            case \"FS\" -> \"Fighting Style Feat\";\n            case \"G\" -> \"General Feat\";\n            case \"O\" -> \"Origin Feat\";\n            case \"AI\" -> \"Artificer Infusion\";\n            case \"ED\" -> \"Elemental Discipline\";\n            case \"EI\" -> \"Eldritch Invocation\";\n            case \"MM\" -> \"Metamagic\";\n            case \"MV\" -> \"Maneuver\";\n            case \"MV:B\" -> \"Maneuver, Battle Master\";\n            case \"MV:C2-UA\" -> \"Maneuver, Cavalier V2 (UA)\";\n            case \"AS:V1-UA\" -> \"Arcane Shot, V1 (UA)\";\n            case \"AS:V2-UA\" -> \"Arcane Shot, V2 (UA)\";\n            case \"AS\" -> \"Arcane Shot\";\n            case \"OTH\" -> \"Other\";\n            case \"FS:F\" -> \"Fighting Style, Fighter\";\n            case \"FS:B\" -> \"Fighting Style, Bard\";\n            case \"FS:P\" -> \"Fighting Style, Paladin\";\n            case \"FS:R\" -> \"Fighting Style, Ranger\";\n            case \"PB\" -> \"Pact Boon\";\n            case \"OR\" -> \"Onomancy Resonant\";\n            case \"RN\" -> \"Rune Knight Rune\";\n            case \"AF\" -> \"Alchemical Formula\";\n            case \"TT\" -> \"Traveler's Trick\";\n            case \"RP\" -> \"Renown Perk\";\n            default -> {\n                if (!homebrew.isEmpty()) {\n                    yield homebrew.values().stream()\n                            .map(hb -> hb.getOptionalFeatureType(featureType))\n                            .distinct()\n                            .collect(Collectors.joining(\"; \"));\n                }\n                yield featureType;\n            }\n        };\n    };\n\n    public static String spellLevelToText(String level) {\n        return switch (level) {\n            case \"0\", \"c\" -> \"cantrip\";\n            case \"1\" -> \"1st-level\";\n            case \"2\" -> \"2nd-level\";\n            case \"3\" -> \"3rd-level\";\n            default -> level + \"th-level\";\n        };\n    }\n\n    @RegisterForReflection\n    @JsonIgnoreProperties(ignoreUnknown = true)\n    class JsonMediaHref {\n        public String type;\n        public JsonHref href;\n        public String title;\n        public Integer width;\n        public Integer height;\n        public String altText;\n        public String credit;\n        public String text;\n    }\n\n    @RegisterForReflection\n    @JsonIgnoreProperties(ignoreUnknown = true)\n    class JsonHref {\n        public String type;\n        public String path;\n        public String url;\n    }\n\n    Map<String, Integer> XP_CHART_ALT = Map.ofEntries(\n            entry(\"0\", 10),\n            entry(\"1/8\", 25),\n            entry(\"1/4\", 50),\n            entry(\"1/2\", 100),\n            entry(\"1\", 200),\n            entry(\"2\", 450),\n            entry(\"3\", 700),\n            entry(\"4\", 1100),\n            entry(\"5\", 1800),\n            entry(\"6\", 2300),\n            entry(\"7\", 2900),\n            entry(\"8\", 3900),\n            entry(\"9\", 5000),\n            entry(\"10\", 5900),\n            entry(\"11\", 7200),\n            entry(\"12\", 8400),\n            entry(\"13\", 10000),\n            entry(\"14\", 11500),\n            entry(\"15\", 13000),\n            entry(\"16\", 15000),\n            entry(\"17\", 18000),\n            entry(\"18\", 20000),\n            entry(\"19\", 22000),\n            entry(\"20\", 25000),\n            entry(\"21\", 33000),\n            entry(\"22\", 41000),\n            entry(\"23\", 50000),\n            entry(\"24\", 62000),\n            entry(\"25\", 75000),\n            entry(\"26\", 90000),\n            entry(\"27\", 105000),\n            entry(\"28\", 120000),\n            entry(\"29\", 135000),\n            entry(\"30\", 155000));\n\n    public enum AbilityScoreFields implements JsonNodeReader {\n        str(),\n        dex(),\n        con(),\n        intel(\"int\"),\n        wis(),\n        cha(),\n        ;\n\n        private final String altName;\n\n        AbilityScoreFields() {\n            this(null);\n        }\n\n        AbilityScoreFields(String altName) {\n            this.altName = altName;\n        }\n\n        public String nodeName() {\n            return altName == null ? name() : altName;\n        }\n    }\n\n    enum Tools5eFields implements JsonNodeReader {\n        _monsterFluff,\n        abbreviation,\n        additionalEntries,\n        additionalSources,\n        alternate,\n        amount,\n        appliesTo,\n        attributes,\n        basicRules,\n        by,\n        className,\n        classSource,\n        condition, // speed, ac\n        count,\n        cr,\n        data, // statblock, statblockInline\n        dataType, // statblockInline\n        deck,\n        edition,\n        entriesTemplate,\n        familiar,\n        featureType,\n        fluff,\n        group,\n        hasFluff,\n        hasFluffImages,\n        hasToken,\n        hidden,\n        id,\n        images,\n        items,\n        lairActions, // legendary group\n        level,\n        number, // speed\n        optionalfeature,\n        otherSources,\n        parentSource,\n        prop, // statblock\n        race,\n        regionalEffects, // legendary group\n        shortName,\n        size,\n        sort, // monsters, vehicles (sorted traits)\n        speed,\n        srd,\n        style,\n        subclass,\n        subrace,\n        tables, // for optfeature types\n        tag, // statblock\n        template,\n        text,\n        tokenHref,\n        tokenUrl,\n        traitTags,\n        visible,\n        xp,\n    }\n\n    enum TableFields implements JsonNodeReader {\n        tables,\n        caption,\n        colLabels,\n        colLabelGroups,\n        colLabelRows,\n        colStyles,\n        rowLabels,\n        rows,\n        row,\n        footnotes,\n        intro,\n        outro,\n        style,\n        type,\n        ttrpgNestedTable, // mine, array\n        ;\n\n        static String getFirstRow(JsonNode tableNode) {\n            JsonNode rowData = rows.getFrom(tableNode);\n            if (rowData == null || rowData.isNull() || rowData.size() == 0) {\n                return \"\";\n            }\n            return rowData.get(0).toString();\n        }\n    }\n\n    enum AlignmentFields implements JsonNodeReader {\n        alignment,\n        chance,\n        note,\n        special\n    }\n\n    enum AttackFields implements JsonNodeReader {\n        attackType,\n        attackEntries,\n        hitEntries,\n    }\n\n    enum RollFields implements JsonNodeReader {\n        roll,\n        exact,\n        min,\n        max\n    }\n\n    enum AppendTypeValue implements JsonNodeReader.FieldValue {\n        // recursive\n        entries,\n        entry,\n        inset,\n        insetReadaloud,\n        list,\n        optfeature,\n        options,\n        quote,\n        section,\n        table,\n        tableGroup,\n        variant,\n        variantInner,\n        variantSub,\n\n        // block\n        abilityAttackMod,\n        abilityDc,\n        abilityGeneric,\n\n        // inline\n        inline,\n        inlineBlock,\n        link,\n\n        // list items\n        item,\n        itemSpell,\n        itemSub,\n\n        // images\n        image,\n        gallery,\n\n        // flowchart\n        flowchart,\n        // TODO: flowBlock,\n\n        // embedded entities\n        statblock,\n        statblockInline,\n\n        refClassFeature,\n        refOptionalfeature,\n        refSubclassFeature,\n\n        // homebrew changes\n        homebrew,\n\n        // attack / action entries\n        attack,\n\n        // misc\n        hr;\n\n        @Override\n        public String value() {\n            return this.name();\n        }\n\n        static AppendTypeValue valueFrom(JsonNode source, JsonNodeReader field) {\n            String textOrNull = field.getTextOrNull(source);\n            if (textOrNull == null) {\n                return null;\n            }\n            return Stream.of(AppendTypeValue.values())\n                    .filter((t) -> t.matches(textOrNull))\n                    .findFirst().orElse(null);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/JsonTextReplacement.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static dev.ebullient.convert.StringUtil.asModifier;\nimport static dev.ebullient.convert.StringUtil.intOrDefault;\nimport static dev.ebullient.convert.StringUtil.isPresent;\nimport static dev.ebullient.convert.StringUtil.joinConjunct;\nimport static dev.ebullient.convert.StringUtil.toOrdinal;\nimport static dev.ebullient.convert.StringUtil.toTitleCase;\nimport static dev.ebullient.convert.StringUtil.valueOrDefault;\n\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.function.Function;\nimport java.util.regex.MatchResult;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.ArrayNode;\n\nimport dev.ebullient.convert.config.CompendiumConfig;\nimport dev.ebullient.convert.config.CompendiumConfig.DiceRoller;\nimport dev.ebullient.convert.config.TtrpgConfig;\nimport dev.ebullient.convert.io.Msg;\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.tools.JsonTextConverter;\nimport dev.ebullient.convert.tools.dnd5e.OptionalFeatureIndex.OptionalFeatureCondition;\nimport dev.ebullient.convert.tools.dnd5e.qute.AbilityScores;\n\npublic interface JsonTextReplacement extends JsonTextConverter<Tools5eIndexType> {\n    static final Pattern FRACTIONAL = Pattern.compile(\"^(\\\\d+)?([⅛¼⅜½⅝¾⅞⅓⅔⅙⅚])?$\");\n    static final Pattern linkifyPattern = Pattern.compile(\"\\\\{@(\"\n            + \"|action|background|card|class|condition|creature|creatureFluff|deck|deity|disease|facility\"\n            + \"|feat|hazard|item|itemMastery|itemProperty|itemType|legroup|object|psionic|race|reward\"\n            + \"|sense|skill|spell|status|subclass|table|variantrule|vehicle\"\n            + \"|optfeature|classFeature|subclassFeature|trap) ([^}]+)}\");\n    static final Pattern chancePattern = Pattern.compile(\"\\\\{@chance ([^}]+)}\");\n    static final Pattern fontPattern = Pattern.compile(\"\\\\{@font ([^}]+)}\");\n    static final Pattern homebrewPattern = Pattern.compile(\"\\\\{@homebrew ([^}]+)}\");\n    static final Pattern linkTo5eImgRepo = Pattern.compile(\"\\\\{@5etoolsImg ([^}]+)}\");\n    static final Pattern quickRefPattern = Pattern.compile(\"\\\\{@quickref ([^}]+)}\");\n    static final Pattern notePattern = Pattern.compile(\"\\\\{@(note|tip) ([^}]+)}\");\n    static final Pattern footnotePattern = Pattern.compile(\"\\\\{@footnote ([^}]+)}\");\n    static final Pattern abilitySavePattern = Pattern.compile(\"\\\\{@(ability|savingThrow) ([^}]+)}\"); // {@ability str 20}\n    static final Pattern savingThrowPattern = Pattern.compile(\"\\\\{@actSave ([^}]+)}\");\n    static final Pattern actSaveFailPattern = Pattern.compile(\"\\\\{@actSaveFail ?([^}]+)?}\");\n    static final Pattern actResponse = Pattern.compile(\"\\\\{@actResponse ?([^}]+)?}\");\n    static final Pattern attackPattern = Pattern.compile(\"\\\\{@atkr? ([^}]+)}\");\n    static final Pattern skillCheckPattern = Pattern.compile(\"\\\\{@skillCheck ([^}]+)}\"); // {@skillCheck animal_handling\n                                                                                         // 5}\n    static final Pattern optionalFeaturesFilter = Pattern.compile(\"\\\\{@filter ([^|}]+)\\\\|optionalfeatures\\\\|([^}]*)}\");\n    static final Pattern superscriptCitationPattern = Pattern.compile(\"\\\\{@(sup|cite) ([^}]+)}\");\n    static final Pattern promptPattern = Pattern.compile(\"#\\\\$prompt_number(?::(.*?))?\\\\$#\");\n    static final String subclassFeatureMask = \"subclassfeature\\\\|(.*)\\\\|.*?\\\\|.*?\\\\|.*?\\\\|.*?\\\\|(\\\\d+)\\\\|.*\";\n\n    static final Set<String> missingKeys = new HashSet<>();\n\n    Tools5eIndex index();\n\n    Tools5eSources getSources();\n\n    default Tui tui() {\n        return cfg().tui();\n    }\n\n    default CompendiumConfig cfg() {\n        return index().cfg();\n    }\n\n    default boolean useCompendium() {\n        return getSources().getType().useCompendiumBase();\n    }\n\n    default Tools5eLinkifier linkifier() {\n        return Tools5eLinkifier.instance();\n    }\n\n    default String getImagePath() {\n        Tools5eIndexType type = getSources().getType();\n        return linkifier().getRelativePath(type);\n    }\n\n    default List<String> findAndReplace(JsonNode jsonSource, String field) {\n        return findAndReplace(jsonSource, field, s -> s);\n    }\n\n    default List<String> findAndReplace(JsonNode jsonSource, String field, Function<String, String> replacement) {\n        JsonNode node = jsonSource.get(field);\n        if (node == null || node.isNull()) {\n            return List.of();\n        } else if (node.isTextual()) {\n            return List.of(replaceText(node.asText()));\n        } else if (node.isObject()) {\n            throw new IllegalArgumentException(\n                    \"Unexpected object node (expected array): %s (referenced from %s)\".formatted(\n                            node,\n                            getSources()));\n        }\n        return streamOf(jsonSource.withArray(field))\n                .map(x -> replaceText(x.asText()).trim())\n                .map(replacement)\n                .filter(x -> !x.isBlank())\n                .collect(Collectors.toList());\n    }\n\n    default String joinAndReplace(JsonNode jsonSource, String field) {\n        JsonNode node = jsonSource.get(field);\n        if (node == null || node.isNull()) {\n            return \"\";\n        } else if (node.isTextual()) {\n            return node.asText();\n        } else if (node.isObject()) {\n            throw new IllegalArgumentException(\n                    \"Unexpected object node (expected array): %s (referenced from %s)\".formatted(\n                            node,\n                            getSources()));\n        }\n        return joinAndReplace((ArrayNode) node);\n    }\n\n    default String joinAndReplace(ArrayNode array) {\n        List<String> list = new ArrayList<>();\n        array.forEach(v -> list.add(replaceText(v.asText())));\n        return String.join(\", \", list);\n    }\n\n    default String replaceText(String input) {\n        return replaceTokens(input, (s, b) -> this._replaceTokenText(s, b));\n    }\n\n    default String tableHeader(String x) {\n        if (x.contains(\"dice\")) {\n            // don't do the usual dice formatting in a column header\n            x = replacePromptStrings(x);\n            if (x.endsWith(\"Card}\")) {\n                x = x.replaceAll(\"\\\\{@dice ([^}|]+)\\\\|?([^}]*)}\", \"$1 | $2\");\n            } else {\n                x = x.replaceAll(\"\\\\{@dice ([^}|]+)\\\\|?[^}]*}\", \"$1\");\n            }\n            x = replaceText(x);\n        } else {\n            x = replaceText(x);\n        }\n        if (x.matches(\"^\\\\d*d\\\\d+( \\\\|.*)?$\")) {\n            return \"dice: \" + x;\n        }\n        return x;\n    }\n\n    default String replacePromptStrings(String s) {\n        return promptPattern.matcher(s).replaceAll((match) -> {\n            List<String> prompts = new ArrayList<>();\n            String title = null;\n            String[] parts = match.group(1).split(\",\");\n            for (String t : parts) {\n                if (t.startsWith(\"title=\")) {\n                    title = t.substring(6)\n                            .replaceAll(\"^Enter ?(a|your|the)? \", \"\")\n                            .replace(\"!\", \"\")\n                            .trim();\n                } else {\n                    prompts.add(t);\n                }\n            }\n            if (title == null) {\n                for (String x : prompts) {\n                    if (x.startsWith(\"default=\")) {\n                        title = x.substring(8);\n                        prompts.remove(x);\n                        break;\n                    }\n                }\n            }\n            prompts.sort(String::compareToIgnoreCase);\n            return \"<span%s>[%s]</span>\".formatted(\n                    prompts.isEmpty() ? \"\" : \" title='\" + String.join(\", \", prompts) + \"'\",\n                    title);\n        });\n    }\n\n    default String _replaceTokenText(String input, boolean nested) {\n        String result = input;\n\n        // render.js this._renderString_renderTag\n        try {\n            result = replacePromptStrings(result);\n\n            // {@dice .. }, {@damage ..}{@hit ..}, {@d20 ..}, {@initiative ...},\n            // {@scaledice..}, {@scaledamage..}\n            result = replaceWithDiceRoller(result);\n\n            result = chancePattern.matcher(result).replaceAll((match) -> {\n                // \"Chance tags; similar to dice roller tags, but output success/failure.\n                // {@chance 50}; {@chance 50|display text}; {@chance 50|display text|rolled by\n                // name};\n                // {@chance 50|display text|rolled by name|on success text};\n                // {@chance 50|display text|rolled by name|on success text|on failure text}.\",\n                String[] parts = match.group(1).split(\"\\\\|\");\n                return parts.length > 1\n                        ? parts[1]\n                        : parts[0] + \" percent\";\n            });\n\n            result = abilitySavePattern.matcher(result).replaceAll(this::replaceSkillOrAbility);\n            result = skillCheckPattern.matcher(result).replaceAll(this::replaceSkillCheck);\n            result = savingThrowPattern.matcher(result).replaceAll(this::replaceSavingThrow);\n            result = actSaveFailPattern.matcher(result).replaceAll(this::replaceActSaveFail);\n            result = actResponse.matcher(result).replaceAll((match) -> {\n                // {@actResponse}\n                // {@actResponse d}*Wisdom\n                // textStack[0] += `<i>Response${text.includes(\"d\") ? \"\\u2014\" : \":\"}</i>`;\n                String param = match.group(1);\n                // use underscores here, it often bumps directly against other italic text\n                return \"_Response%s_\".formatted(param != null && param.contains(\"d\") ? \"—\" : \":\");\n            });\n\n            result = superscriptCitationPattern.matcher(result).replaceAll((match) -> {\n                // {@sup {@cite Casting Times|FleeMortals|A}}\n                // {@sup whatever}\n                // {@cite Casting Times|FleeMortals|A}\n                // {@cite Casting Times|FleeMortals|{@sup A}}\n                if (match.group(1).equals(\"sup\")) {\n                    String text = replaceText(match.group(2));\n                    if (text.startsWith(\"[^\") || text.startsWith(\"^[\")) {\n                        // do not put citations in superscript (obsidian/markdown will do it)\n                        return text;\n                    }\n                    return \"<sup>\" + text + \"</sup>\";\n                }\n                return handleCitation(match.group(2));\n            });\n\n            result = homebrewPattern.matcher(result).replaceAll((match) -> {\n                // {@homebrew changes|modifications}, {@homebrew additions} or {@homebrew\n                // |removals}\n                String s = match.group(1);\n                int pos = s.indexOf('|');\n                if (pos == 0) { // removal\n                    return \"[...] ^[The following text has been removed with this homebrew: \" + s.substring(1) + \"]\";\n                } else if (pos < 0) { // addition\n                    return s + \" ^[This is a homebrew addition]\";\n                }\n                String oldText = s.substring(0, pos);\n                String newText = s.substring(pos + 1);\n\n                return newText + \" ^[This is a homebrew addition, replacing the following: \" + oldText + \"]\";\n            });\n\n            result = linkTo5eImgRepo.matcher(result).replaceAll((match) -> {\n                // External links to materials in the 5eTools image repo (usually pdf):\n                // {@5etoolsImg Players Handbook Cover|covers/PHB.webp}\n                // const fauxEntry = {\n                //     type: \"link\",\n                //     href: {\n                //         type: \"external\",\n                //         url: UrlUtil.link(this.getMediaUrl(\"img\", page)),\n                //     },\n                //     text: displayText,\n                // };\n                String orig = match.group(0);\n                if (!orig.contains(\"|\")) {\n                    return orig;\n                }\n\n                String[] parts = match.group(1).split(\"\\\\|\");\n                String imgRepo = TtrpgConfig.getConstant(TtrpgConfig.DEFAULT_IMG_ROOT);\n                String url = ImageRef.Builder.fixUrl(imgRepo + (imgRepo.endsWith(\"/\") ? \"\" : \"/\") + parts[1]);\n\n                return \"[%s](%s)\".formatted(parts[0], url);\n            });\n\n            result = linkifyPattern.matcher(result)\n                    .replaceAll(this::linkify);\n\n            result = optionalFeaturesFilter.matcher(result)\n                    .replaceAll(this::linkifyOptionalFeatureType);\n\n            result = quickRefPattern.matcher(result).replaceAll((match) -> {\n                String[] parts = match.group(1).split(\"\\\\|\");\n                if (parts.length > 4) {\n                    return parts[4];\n                }\n                return parts[0];\n            });\n\n            result = fontPattern.matcher(result).replaceAll((match) -> {\n                String[] parts = match.group(1).split(\"\\\\|\");\n                String fontFamily = Tools5eSources.getFontReference(parts[1]);\n                if (fontFamily != null) {\n                    return \"<span style=\\\"font-family: %s\\\">%s</span>\".formatted(\n                            fontFamily, parts[0]);\n                }\n                return parts[0];\n            });\n\n            result = attackPattern.matcher(result).replaceAll((match) -> {\n                List<String> type = new ArrayList<>();\n                String method = \"\";\n                // render.js Renderer.attackTagToFull\n                // const ptType = tags.includes(\"m\") ? \"Melee \" : tags.includes(\"r\") ? \"Ranged \"\n                // : tags.includes(\"g\") ? \"Magical \" : tags.includes(\"a\") ? \"Area \" : \"\";\n                // const ptMethod = tags.includes(\"w\") ? \"Weapon \" : tags.includes(\"s\") ? \"Spell\n                // \" : tags.includes(\"p\") ? \"Power \" : \"\";\n                if (match.group(1).contains(\"m\")) {\n                    type.add(\"Melee \");\n                }\n                if (match.group(1).contains(\"r\")) {\n                    type.add(\"Ranged \");\n                }\n                if (match.group(1).contains(\"g\")) {\n                    type.add(\"Magical \");\n                }\n                if (match.group(1).contains(\"a\")) {\n                    type.add(\"Area \");\n                }\n\n                if (match.group(1).contains(\"w\")) {\n                    method = \"Weapon \";\n                } else if (match.group(1).contains(\"s\")) {\n                    method = \"Spell \";\n                } else if (match.group(1).contains(\"p\")) {\n                    method = \"Power \";\n                }\n\n                if (method.isBlank()) {\n                    return String.format(\"*%sAttack Roll:*\", joinConjunct(\", \", \" or \", type));\n                } else {\n                    return String.format(\"*%s%sAttack:*\", joinConjunct(\", \", \" or \", type), method);\n                }\n            });\n\n            try {\n                result = result\n                        // \"Internal links: {@5etools This Is Your Life|lifegen.html}\",\n                        // \"External links: {@link https://discord.gg/5etools} or {@link\n                        // Discord|https://discord.gg/5etools}\"\n                        .replaceAll(\"\\\\{@link ([^}|]+)\\\\|([^}]+)}\", \"$1 ($2)\") // this must come first\n                        .replaceAll(\"\\\\{@link ([^}|]+)}\", \"$1\") // this must come first\n                        .replaceAll(\"\\\\{@5etools ([^}|]+)\\\\|?[^}]*}\", \"$1\")\n                        .replaceAll(\"\\\\{@5etoolsAudio ([^}|]+)\\\\|?[^}]*}\", \"$1\")\n                        .replaceAll(\"\\\\{@loader ([^}|]+)\\\\|([^}]+)}\", \"$1 ^[$2]\")\n                        .replaceAll(\"\\\\{@area ([^|}]+)\\\\|?[^}]*}\", \"$1\")\n                        .replaceAll(\"\\\\{@vehupgrade ([^|}]+)\\\\|?[^}]*}\", \"$1\") // TODO: vehicle upgrade type\n                        .replaceAll(\"\\\\{@dc ([^}]+)}\", \"DC $1\")\n                        .replaceAll(\"\\\\{@recharge ([^}]+?)}\", \"(Recharge $1-6)\")\n                        .replaceAll(\"\\\\{@recharge}\", \"(Recharge 6)\")\n                        .replaceAll(\"\\\\{@coinflip ([^|}]+)\\\\|?[^}]*}\", \"$1\")\n                        .replaceAll(\"\\\\{@coinflip}\", \"flip a coin\")\n                        .replaceAll(\"\\\\{@filter ([^|}]+)\\\\|?[^}]*}\", \"$1\")\n                        .replaceAll(\"\\\\{@boon ([^|}]+)\\\\|[^|}]+\\\\|([^|}]*)}\", \"$2\")\n                        .replaceAll(\"\\\\{@boon ([^|}]+)\\\\|[^}]*}\", \"$1\")\n                        .replaceAll(\"\\\\{@boon ([^|}]+)}\", \"$1\")\n                        .replaceAll(\"\\\\{@charoption ([^|}]+)\\\\|[^|}]+\\\\|([^|}]*)}\", \"$2\")\n                        .replaceAll(\"\\\\{@charoption ([^|}]+)\\\\|[^}]*}\", \"$1\")\n                        .replaceAll(\"\\\\{@charoption ([^|}]+)}\", \"$1\")\n                        .replaceAll(\"\\\\{@crochet ([^|}]+)\\\\|[^|}]+\\\\|([^|}]*)}\", \"$2\")\n                        .replaceAll(\"\\\\{@crochet ([^|}]+)\\\\|[^}]*}\", \"$1\")\n                        .replaceAll(\"\\\\{@crochet ([^}|]+)}\", \"$1\")\n                        .replaceAll(\"\\\\{@recipe ([^|}]+)\\\\|[^|}]+\\\\|([^|}]*)}\", \"$2\")\n                        .replaceAll(\"\\\\{@recipe ([^|}]+)\\\\|[^}]*}\", \"$1\")\n                        .replaceAll(\"\\\\{@recipe ([^|}]+)}\", \"$1\")\n                        .replaceAll(\"\\\\{@cult ([^|}]+)\\\\|[^|}]+\\\\|([^|}]*)}\", \"$2\")\n                        .replaceAll(\"\\\\{@cult ([^|}]+)\\\\|[^}]*}\", \"$1\")\n                        .replaceAll(\"\\\\{@cult ([^|}]+)}\", \"$1\")\n                        .replaceAll(\"\\\\{@language ([^|}]+)\\\\|[^|}]*\\\\|([^|}]*)}\", \"$2\")\n                        .replaceAll(\"\\\\{@language ([^|}]+)\\\\|[^}]*}\", \"$1\")\n                        .replaceAll(\"\\\\{@language ([^|}]+)}\", \"$1\")\n                        .replaceAll(\"\\\\{@book ([^}|]+)\\\\|?[^}]*}\", \"\\\"$1\\\"\")\n                        .replaceAll(\"\\\\{@h}\", \"*Hit:* \") // render.js Renderer.tag\n                        .replaceAll(\"\\\\{@m}\", \"*Miss:* \")\n                        .replaceAll(\"\\\\{@hom}\", \"*Hit or Miss:* \")// render.js Renderer.tag\n                        .replaceAll(\"\\\\{@actSaveSuccess}\", \"*Success:*\") // render.js Renderer.tag\n                        .replaceAll(\"\\\\{@actSaveSuccessOrFail}\", \"*Failure or Success:*\") // render.js Renderer.tag\n                        .replaceAll(\"\\\\{@actResponse}\", \"Response:\") // render.js Renderer.tag\n                        .replaceAll(\"\\\\{@actTrigger}\", \"Trigger:\") // render.js Renderer.tag\n                        .replaceAll(\"\\\\{@dcYourSpellSave}\", \"your spell save DC\") // render.js Renderer.tag\n                        .replaceAll(\"\\\\{@spell\\\\s*}\", \"\") // error in homebrew\n                        .replaceAll(\"\\\\{@color ([^|}]+)\\\\|?[^}]*}\", \"$1\")\n                        .replaceAll(\"\\\\{@style ([^|}]+)\\\\|?[^}]*}\", \"$1\")\n                        .replaceAll(\"\\\\{@b ([^}]+?)}\", \"**$1**\")\n                        .replaceAll(\"\\\\{@bold ([^}]+?)}\", \"**$1**\")\n                        .replaceAll(\"\\\\{@c ([^}]+?)}\", \"$1\")\n                        .replaceAll(\"\\\\{@center ([^}]+?)}\", \"$1\")\n                        .replaceAll(\"\\\\{@i ([^}]+?)}\", \"*$1*\")\n                        .replaceAll(\"\\\\{@italic ([^}]+)}\", \"*$1*\")\n                        .replaceAll(\"\\\\{@s ([^}]+?)}\", \"~~$1~~\")\n                        .replaceAll(\"\\\\{@strike ([^}]+)}\", \"~~$1~~\")\n                        .replaceAll(\"\\\\{@u ([^}]+?)}\", \"_$1_\")\n                        .replaceAll(\"\\\\{@underline ([^}]+?)}\", \"_$1_\")\n                        .replaceAll(\"\\\\{@comic ([^}]+?)}\", \"$1\")\n                        .replaceAll(\"\\\\{@comicH1 ([^}]+?)}\", \"$1\")\n                        .replaceAll(\"\\\\{@comicH2 ([^}]+?)}\", \"$1\")\n                        .replaceAll(\"\\\\{@comicH3 ([^}]+?)}\", \"$1\")\n                        .replaceAll(\"\\\\{@comicH4 ([^}]+?)}\", \"$1\")\n                        .replaceAll(\"\\\\{@comicNote ([^}]+?)}\", \"$1\")\n                        .replaceAll(\"\\\\{@highlight ([^}]+?)}\", \"==$1==\")\n                        .replaceAll(\"\\\\{@code ([^}]+?)}\", \"`$1`\")\n                        .replaceAll(\"\\\\{@kbd ([^}]+?)}\", \"`$1`\")\n                        .replaceAll(\"\\\\{@b}\", \" \")\n                        .replaceAll(\"\\\\{@i}\", \" \");\n            } catch (Exception e) {\n                tui().errorf(e, \"Unable to parse string from %s: %s\", getSources().getKey(), input);\n            }\n\n            result = footnotePattern.matcher(result).replaceAll((match) -> {\n                // {@footnote directly in text|This is primarily for homebrew purposes, as the\n                // official texts (so far) avoid using footnotes},\n                // {@footnote optional reference information|This is the footnote. References\n                // are free text.|Footnote 1, page 20}.\",\n                // We're converting these to _inline_ markdown footnotes, as numbering is\n                // difficult to track\n                String[] parts = match.group(1).split(\"\\\\|\");\n                if (parts[0].contains(\"<sup>\")) {\n                    // This already assumes what the footnote name will be\n                    // TODO: Note content is lost on this path at the moment\n                    return parts[0];\n                }\n                if (parts.length > 2) {\n                    return \"%s ^[%s, _%s_]\".formatted(parts[0], parts[1], parts[2]);\n                }\n                return \"%s ^[%s]\".formatted(parts[0], parts[1]);\n            });\n\n            result = notePattern.matcher(result).replaceAll((match) -> {\n                return switch (match.group(1)) {\n                    case \"note\" -> {\n                        // {@note This is a note}\n                        if (nested) {\n                            yield \"<span class='note'>**Note:** \" + replaceText(match.group(2).trim()) + \"</span>\";\n                        } else {\n                            List<String> text = new ArrayList<>();\n                            text.add(\"> [!note]\");\n                            for (String line : match.group(2).split(\"\\n\")) {\n                                text.add(\"> \" + replaceText(line.trim()));\n                            }\n                            yield String.join(\"\\n\", text);\n                        }\n                    }\n                    case \"tip\" -> {\n                        // {@tip tooltip tags|a note}\n                        String[] parts = match.group(2).split(\"\\\\|\");\n                        yield \"<span class='tip' title='%s'>%s</span>\".formatted(parts[1], parts[0]);\n                    }\n                    default -> {\n                        yield match.group(0);\n                    }\n                };\n            });\n\n            // after other replacements\n            return result.replaceAll(\"\\\\{@adventure ([^|}]+)\\\\|[^}]*}\", \"$1\");\n\n        } catch (IllegalArgumentException e) {\n            tui().errorf(e, \"Failure replacing text: %s\", e.getMessage());\n        }\n\n        return input;\n    }\n\n    default String replaceSavingThrow(MatchResult match) {\n        // format: {@actSave dex}\n        String key = match.group(1);\n        SkillOrAbility ability = index().findSkillOrAbility(key, getSources());\n\n        return String.format(\"*%s Saving Throw:*\", ability.value());\n    }\n\n    default String replaceActSaveFail(MatchResult match) {\n        // format: {@actSaveFail 1}\n        String ordinal = match.group(1) == null ? null : match.group(1);\n        if (ordinal == null) {\n            return \"*Failure:*\";\n        }\n\n        if (ordinal.contains(\"\\\\|\")) {\n            ordinal = ordinal.split(\"\\\\|\")[0];\n        }\n        return \"*%s Failure:*\".formatted(toTitleCase(toOrdinal(ordinal)));\n        // const [ordinal] = Renderer.splitTagByPipe(text);\n        // if (ordinal) textStack[0] += `*${Parser.numberToText(ordinal, {isOrdinalForm: true}).toTitleCase()} Failure:*`;\n        // else textStack[0] += `*Failure:*`;\n    }\n\n    default String replaceSkillOrAbility(MatchResult match) {\n        // format: {@ability str 20} or {@ability str 20|Display Text}\n        // or {@ability str 20|Display Text|Roll Name Text}\n        // format: {@savingThrow str 5} or {@savingThrow str 5|Display Text}\n        // or {@savingThrow str 5|Display Text|Roll Name Text}\n        DiceRoller roller = cfg().useDiceRoller();\n\n        boolean abilityCheck = match.group(1).equals(\"ability\");\n        String[] parts = match.group(2).split(\"\\\\|\");\n        int pos = parts[0].indexOf(' ');\n        String ability = parts[0].substring(0, pos);\n        String score = parts[0].substring(pos + 1);\n\n        SkillOrAbility abilityScore = index().findSkillOrAbility(ability, getSources());\n\n        final String text;\n        if (!abilityCheck && !score.matches(\"[+-]?\\\\d+\")) {\n            // Saving throws can have e.g. `+ PB`\n            text = valueOrDefault(parts, 1, score);\n        } else {\n            String displayText = valueOrDefault(parts, 1, null);\n            int value = intOrDefault(score, 0);\n            String mod = abilityCheck\n                    ? asModifier(AbilityScores.scoreToModifier(value))\n                    : asModifier(value);\n\n            if (abilityCheck) {\n                text = roller.useDiceRolls(parseState())\n                        ? (displayText == null\n                                ? \"`%s` (`dice:d20%s|noform|noparens|text(%s)`)\".formatted(value, mod, mod)\n                                : \"`dice:d20%s|noform|noparens|text(%s)`\".formatted(value, displayText))\n                        : (displayText == null\n                                ? (roller.decorate(parseState()) ? \"`%s` (`%s`)\" : \"%s (%s)\").formatted(value, mod)\n                                : displayText);\n            } else {\n                // saving throw\n                text = roller.useDiceRolls(parseState())\n                        ? \"`dice:d20%s|noform|noparens|text(%s)`\".formatted(mod, displayText == null ? mod : displayText)\n                        : (displayText == null\n                                ? (roller.decorate(parseState()) ? \"`%s`\" : \"%s\").formatted(mod)\n                                : displayText);\n            }\n        }\n        return \"<span title='%s'>%s</span>\".formatted(abilityScore.value(), text);\n    }\n\n    default String replaceSkillCheck(MatchResult match) {\n        DiceRoller roller = cfg().useDiceRoller();\n\n        // format: {@skillCheck animal_handling 5} or {@skillCheck animal_handling\n        // 5|Display Text}\n        // or {@skillCheck animal_handling 5|Display Text|Roll Name Text}\n        String[] parts = match.group(1).split(\"\\\\|\");\n        String[] score = parts[0].split(\" \");\n\n        SkillOrAbility skill = index().findSkillOrAbility(score[0], getSources());\n        String text = valueOrDefault(parts, 1, linkifySkill(skill));\n\n        String dice = score[1];\n        if (score[1].matches(\"\\\\d+\")) {\n            int value = Integer.parseInt(score[1]);\n            dice = \"%s%s\".formatted(value >= 0 ? \"+\" : \"\", value);\n        }\n\n        if (roller.useDiceRolls(parseState())) {\n            dice = \"`dice:1d20%s|noform|noparens|text(%s)`\".formatted(dice, dice);\n        } else if (roller.decorate(parseState())) {\n            dice = \"`\" + dice + \"`\";\n        }\n\n        return \"%s (%s)\".formatted(text, dice);\n    }\n\n    default String linkifySkill(SkillOrAbility skill) {\n        String key = index().getAliasOrDefault(\n                Tools5eIndexType.skill.createKey(skill.value(), skill.source()));\n        return linkifier().link(skill.value(), key);\n    }\n\n    default String linkifyRules(Tools5eIndexType type, String text) {\n        // {@condition stunned} assumes PHB by default,\n        // {@condition stunned|PHB} can have sources added with a pipe (not that it's\n        // ever useful),\n        // {@condition stunned|PHB|and optional link text added with another pipe}.\",\n\n        String[] parts = text.split(\"\\\\|\");\n        String name = parts[0];\n        String source = valueOrDefault(parts, 1, type.defaultSourceString());\n        String linkText = valueOrDefault(parts, 2, name);\n\n        if (name.isBlank()) {\n            String relativePath = linkifier().getRelativePath(type);\n            String docPath = TtrpgConfig.getConfig().splitRules()\n                    ? relativePath + \"/\" + relativePath\n                    : relativePath;\n            return \"[%s](%s%s.md)\".formatted(linkText,\n                    index().rulesVaultRoot(),\n                    docPath);\n        }\n\n        String aliasKey = index().getAliasOrDefault(type.createKey(name, source));\n        return linkifier().link(linkText, aliasKey);\n    }\n\n    default String linkify(MatchResult match) {\n        Tools5eIndexType type = Tools5eIndexType.fromText(match.group(1));\n        if (type == null) {\n            throw new IllegalArgumentException(\"Unable to linkify \" + match.group(0));\n        }\n        return linkify(type, match.group(2));\n    }\n\n    default String linkify(Tools5eIndexType type, String s) {\n        if (!isPresent(s)) {\n            return \"\";\n        }\n        return switch (type) {\n            // {@background Charlatan} assumes PHB by default,\n            // {@background Anthropologist|toa} can have sources added with a pipe,\n            // {@background Anthropologist|ToA|and optional link text added with another\n            // pipe}.\",\n            // {@feat Alert} assumes PHB by default,\n            // {@feat Elven Accuracy|xge} can have sources added with a pipe,\n            // {@feat Elven Accuracy|xge|and optional link text added with another pipe}.\",\n            // {@deck Tarokka Deck|CoS|tarokka deck} // like items\n            // {@hazard brown mold} assumes DMG by default,\n            // {@hazard russet mold|vgm} can have sources added with a pipe,\n            // {@hazard russet mold|vgm|and optional link text added with another pipe}.\",\n            // {@item alchemy jug} assumes DMG by default,\n            // {@item longsword|phb} can have sources added with a pipe,\n            // {@item longsword|phb|and optional link text added with another pipe}.\",\n            // {@legroup unicorn} assumes MM by default,\n            // {@legroup balhannoth|MPMM} can have sources added with a pipe,\n            // {@legroup balhannoth|MPMM|and optional link text added with another pipe}.\",\n            // {@object Ballista} assumes DMG by default,\n            // {@object Ballista|DMG|and optional link text added with another pipe}.\",\n            // {@optfeature Agonizing Blast} assumes PHB by default,\n            // {@optfeature Aspect of the Moon|xge} can have sources added with a pipe,\n            // {@optfeature Aspect of the Moon|xge|and optional link text added with another\n            // pipe}.\",\n            // {@psionic Mastery of Force} assumes UATheMysticClass by default\n            // {@psionic Mastery of Force|UATheMysticClass} can have sources added with a\n            // pipe\n            // {@psionic Mastery of Force|UATheMysticClass|and optional link text added with\n            // another pipe}.\",\n            // {@race Human} assumes PHB by default,\n            // {@race Aasimar (Fallen)|VGM}\n            // {@race Aasimar|DMG|racial traits for the aasimar}\n            // {@race Aarakocra|eepc} can have sources added with a pipe,\n            // {@race Aarakocra|eepc|and optional link text added with another pipe}.\",\n            // {@race dwarf (hill)||Dwarf, hill}\n            // {@reward Blessing of Health} assumes DMG by default,\n            // {@reward Blessing of Health} can have sources added with a pipe,\n            // {@reward Blessing of Health|DMG|and optional link text added with another\n            // pipe}.\",\n            // {@spell acid splash} assumes PHB by default,\n            // {@spell tiny servant|xge} can have sources added with a pipe,\n            // {@spell tiny servant|xge|and optional link text added with another pipe}.\",\n            // {@table 25 gp Art Objects} assumes DMG by default,\n            // {@table Adventuring Gear|phb} can have sources added with a pipe,\n            // {@table Adventuring Gear|phb|and optional link text added with another\n            // pipe}.\",\n            // {@trap falling net} assumes DMG by default,\n            // {@trap falling portcullis|xge} can have sources added with a pipe,\n            // {@trap falling portcullis|xge|and optional link text added with another\n            // pipe}.\",\n            // {@vehicle Galley} assumes GoS by default,\n            // {@vehicle Galley|UAOfShipsAndSea} can have sources added with a pipe,\n            // {@vehicle Galley|GoS|and optional link text added with another pipe}.\",\n            case background,\n                    feat,\n                    classtype,\n                    deck,\n                    facility,\n                    hazard,\n                    item,\n                    legendaryGroup,\n                    monster,\n                    object,\n                    optfeature,\n                    psionic,\n                    race,\n                    reward,\n                    spell,\n                    table,\n                    tableGroup,\n                    trap,\n                    vehicle ->\n                linkifyThreePart(type, s);\n            case card,\n                    deity ->\n                linkifyFourPart(type, s);\n            case action,\n                    condition,\n                    disease,\n                    sense,\n                    skill,\n                    status ->\n                linkifyRules(type, s);\n            case itemMastery, itemProperty, itemType -> linkifyItemAttribute(type, s);\n            case subclass -> linkifySubclass(s); // RARE!!\n            case classfeature -> linkifyClassFeature(s);\n            case subclassFeature -> linkifySubclassFeature(s);\n            case variantrule -> linkifyVariantRule(s);\n            default -> {\n                tui().debugf(Msg.UNKNOWN, \"unknown tag/type {@%s %s} from %s\",\n                        type, s, parseState().getSource());\n                yield s;\n            }\n        };\n    }\n\n    default String linkifyThreePart(Tools5eIndexType type, String match) {\n        // {@legroup balhannoth|MPMM|and optional link text added with another pipe}.\",\n        String[] parts = match.split(\"\\\\|\");\n        String linkText = valueOrDefault(parts, 2, parts[0]);\n\n        // the actual type might change. Work forward off the resolved key\n        // optfeature (2014) -> feat (2024), etc.\n        String key = index().getAliasOrDefault(\n                type.fromTagReference(match));\n\n        return linkifier().link(linkText, key);\n    }\n\n    default String linkifyFourPart(Tools5eIndexType type, String match) {\n        // {@card Donjon|Deck of Several Things|LLK}\n        String[] parts = match.split(\"\\\\|\");\n        String linkText = valueOrDefault(parts, 3, parts[0]);\n        String key = index().getAliasOrDefault(type.fromTagReference(match));\n\n        return linkifier().link(linkText, key);\n    }\n\n    default String linkifyLegendaryGroup(Tools5eSources lgSources) {\n        if (lgSources == null) {\n            return \"\";\n        }\n        return linkifier().link(lgSources);\n    }\n\n    default String linkifyClassFeature(String match) {\n        // \"Class Features: Class source is assumed to be PHB, class feature source is\n        // assumed to be the same as class source\"\n        // {@classFeature Rage|Barbarian||1},\n        // {@classFeature Infuse Item|Artificer|TCE|2},\n        // {@classFeature Survival Instincts|Barbarian||2|UAClassFeatureVariants},\n        // {@classFeature Rage|Barbarian||1||optional display text}.\n        String[] parts = match.split(\"\\\\|\");\n        String linkText = valueOrDefault(parts, 5, parts[0]);\n\n        String classFeatureKey = index().getAliasOrDefault(\n                Tools5eIndexType.classfeature.fromTagReference(match));\n        return linkifier().link(linkText, classFeatureKey);\n    }\n\n    default String linkifyOptionalFeatureType(MatchResult match) {\n        // {@filter display text|page_without_file_extension|filter_name_1=filter_1_value_1;filter_1_value_2;...filter_1_value_n|...|filter_name_m=filter_m_value_1;filter_m_value_2;...}\n        String linkText = match.group(1);\n\n        Map<String, List<String>> conditions = new HashMap<>();\n        for (var condition : match.group(2).split(\"\\\\|\")) {\n            String[] parts = condition.split(\"=\");\n            if (parts.length != 2) {\n                tui().warnf(Msg.UNKNOWN, \"Unable to parse condition of optional feature filter %s from %s\", condition,\n                        match.group(2));\n                return linkText;\n            }\n            conditions.put(parts[0].toLowerCase(), List.of(parts[1].split(\"(,|;)\")));\n        }\n\n        if (conditions.isEmpty()) {\n            return linkText;\n        }\n\n        // Simple/common case: Single feature type as the only condition. Just link to it.\n        // Examples:\n        // {@filter Expanded Traits|optionalfeatures|Feature Type=ET}\n        // {@filter eldritch invocation|optionalfeatures|feature type=EI}\n        List<String> featureType = conditions.getOrDefault(\"feature type\", List.of());\n        if (conditions.size() == 1 && featureType.size() == 1) {\n            return linkifier().linkOptionalFeature(linkText, featureType.get(0));\n        }\n\n        // The not-so-simple case: Multiple conditions, or multiple values for a single condition.\n        // Examples:\n        // {@filter here|optionalfeatures|source=SCoC|Feature Type=BB}\n        // {@filter Elemental Enhancement|optionalfeatures|feature type=Secret Art|level=weaveknight level 5|search=elemental}\n        // {@filter Transmute Armament Secret Art|optionalfeatures|feature type=Secret Art|level=weaveknight level 5;weaveknight level 9|search=transmute}\n        // Note:\n        // Should only split type by semicolon, but at least one homebrew uses a comma instead. Be generous.\n        // {@filter upgrade|optionalfeatures|Feature Type=IS:C;IS:F;IS:G;IS:I;IS:O;IS:P;IS:T;IS:Re;IS:Ru;IS:W}\n        // {@filter Optional Features|optionalfeatures|source=GH|feature type=BGT:Ac,BGT:Ar,BGT:Cl,BGT:CF,BGT:Cr,BGT:M,BGT:O,BGT:S}\n\n        List<OptionalFeatureCondition> conditionsList = new ArrayList<>();\n        for (var entry : conditions.entrySet()) {\n            String key = entry.getKey();\n            switch (key) {\n                case \"feature type\" -> {\n                    OptionalFeatureCondition types = new OptionalFeatureCondition(\n                            1, \"from\", entry.getValue(),\n                            type -> linkifier().linkOptionalFeature(type, type));\n                    conditionsList.add(types);\n                }\n                case \"class\" -> {\n                    OptionalFeatureCondition classes = new OptionalFeatureCondition(\n                            2, \"for classes\", entry.getValue(),\n                            classFilter -> classFilter);\n                    conditionsList.add(classes);\n                }\n                case \"level\" -> {\n                    OptionalFeatureCondition levels = new OptionalFeatureCondition(\n                            3, \"at levels\", entry.getValue(),\n                            level -> level);\n                    conditionsList.add(levels);\n                }\n                case \"source\" -> {\n                    OptionalFeatureCondition sources = new OptionalFeatureCondition(\n                            4, \"defined in\", entry.getValue(),\n                            src -> TtrpgConfig.sourceToLongName(src));\n                    conditionsList.add(sources);\n                }\n                default -> {\n                    tui().warnf(Msg.UNKNOWN, \"Unknown condition %s in optional feature filter %s\", key, match.group(2));\n                }\n            }\n        }\n\n        conditionsList.sort(Comparator.comparingInt(OptionalFeatureCondition::order));\n        return linkText + \"^[Optional features \"\n                + conditionsList.stream()\n                        .map(OptionalFeatureCondition::toString)\n                        .collect(Collectors.joining(\"; \"))\n                + \"]\";\n    }\n\n    default String linkifySubclass(String match) {\n        // Only used in homebrew (so far)\n        // \"Subclasses:{@subclass Berserker|Barbarian},\n        // {@subclass Berserker|Barbarian},\n        // {@subclass Ancestral Guardian|Barbarian||XGE},\n        // {@subclass Artillerist|Artificer|TCE|TCE}.\n        // Class and subclass source is assumed to be PHB.\"\n        String[] parts = match.split(\"\\\\|\");\n        String linkText = valueOrDefault(parts, 4, parts[0]);\n\n        // \"subclass|path of wild magic|barbarian|phb|phb\"\n        String key = index().getAliasOrDefault(\n                Tools5eIndexType.subclass.fromTagReference(match));\n        return linkifier().link(linkText, key);\n    }\n\n    default String linkifySubclassFeature(String match) {\n        // \"Subclass Features:\n        // {@subclassFeature Path of the Berserker|Barbarian||Berserker||3},\n        // {@subclassFeature Alchemist|Artificer|TCE|Alchemist|TCE|3},\n        // {@subclassFeature Path of the Battlerager|Barbarian||Battlerager|SCAG|3}, -->\n        // \"barbarian-path-of-the-... \"\n        // {@subclassFeature Blessed Strikes|Cleric||Life||8|UAClassFeatureVariants},\n        // --> \"-domain\"\n        // {@subclassFeature Blessed Strikes|Cleric|PHB|Twilight|TCE|8|TCE}\n        // {@subclassFeature Path of the Berserker|Barbarian||Berserker||3||optional\n        // display text}.\n        // Class source is assumed to be PHB.\n        // Subclass source is assumed to be PHB.\n        // Subclass feature source is assumed to be the same as subclass source.\",\n        String[] parts = match.split(\"\\\\|\");\n        String linkText = valueOrDefault(parts, 7, parts[0]);\n\n        // Get the right subclass feature key\n        String featureKey = Tools5eIndexType.subclassFeature.fromTagReference(match);\n        featureKey = index().getAliasOrDefault(featureKey);\n        if (featureKey == null || index().isExcluded(featureKey)) {\n            return linkText;\n        }\n\n        // Find the subclass that will be emitted...\n        String subclassKey = Tools5eIndexType.subclass.fromChildKey(featureKey);\n\n        // look up alias for subclass so link is correct, but don't follow reprints\n        // \"subclass|redemption|paladin|phb|\"\n        //    : \"subclass|oath of redemption|paladin|phb|\",\n        // \"subclass|twilight|cleric|phb|tce\"\n        //    : \"subclass|twilight domain|cleric|phb|tce\"\n        subclassKey = index().getAliasOrDefault(subclassKey, false);\n\n        JsonNode subclassNode = index().getNode(subclassKey);\n        if (subclassNode == null) {\n            // if the subclass was reprinted, the target file name will change (minimally)\n            subclassKey = index().getAliasOrDefault(subclassKey);\n            subclassNode = index().getNode(subclassKey);\n            if (subclassNode == null) {\n                tui().warnf(Msg.UNRESOLVED, \"Subclass %s not found for {@subclassfeature %s} in %s\",\n                        subclassKey, match, getSources().getKey());\n                return linkText;\n            }\n            // Examine new subclass node's features, to see if there is a match\n            // e.g. for\n            //   \"subclassfeature|primal companion|ranger|phb|beast master|phb|3|tce\",\n            // consider\n            //   \"subclassfeature|primal companion|ranger|xphb|beast master|xphb|3|xphb\"\n            String test = featureKey.replaceAll(subclassFeatureMask, \"$1-$2\");\n            boolean found = false;\n            for (String fkey : index().findClassFeatures(subclassKey)) {\n                String compare = fkey.replaceAll(subclassFeatureMask, \"$1-$2\");\n                if (test.equals(compare)) {\n                    featureKey = fkey;\n                    found = true;\n                    break;\n                }\n            }\n            if (!found) {\n                tui().warnf(Msg.UNRESOLVED,\n                        \"No equivalent subclass feature found for {@subclassfeature %s} in %s (from %s)\",\n                        match, subclassKey, getSources().getKey());\n                return linkText;\n            }\n        }\n\n        JsonNode featureJson = index().getNode(featureKey);\n        return linkifier().linkSubclassFeature(linkText,\n                featureKey, featureJson,\n                subclassKey, subclassNode);\n    }\n\n    default String linkifyVariantRule(String variant) {\n        // \"fromVariant\": \"Action Options\",\n        // \"fromVariant\": \"Spellcasting|XGE\",\n        Tools5eIndexType type = Tools5eIndexType.variantrule;\n        String[] parts = variant.trim().split(\"\\\\|\");\n        String source = valueOrDefault(parts, 1, Tools5eIndexType.variantrule.defaultSourceString());\n        String linkText = valueOrDefault(parts, 2, parts[0])\n                .replaceAll(\"\\\\[.*\\\\]\", \"\");\n\n        String key = findKey(type, parts[0], source);\n\n        // Check if the key resolved to a reference type (via alias)\n        Tools5eIndexType resolvedType = Tools5eIndexType.getTypeFromKey(key);\n        if (resolvedType == Tools5eIndexType.reference) {\n            return linkifyReference(linkText, key);\n        }\n\n        if (index().isExcluded(key)) {\n            return \"<span title=\\\"%s\\\">%s</span>\".formatted(TtrpgConfig.sourceToLongName(source), linkText);\n        }\n\n        return linkifier().link(linkText, key);\n    }\n\n    default String linkifyReference(String linkText, String key) {\n        String[] parts = key.split(\"\\\\|\");\n        String name = parts.length > 1 ? parts[1] : key;\n\n        Tools5eIndex index = index();\n        String path = switch (name.toLowerCase()) {\n            case \"item mastery\" -> {\n                String relativePath = linkifier().getRelativePath(Tools5eIndexType.itemMastery);\n                if (TtrpgConfig.getConfig().splitRules()) {\n                    relativePath = relativePath + \"/\" + relativePath;\n                }\n                yield relativePath;\n            }\n            case \"item properties\" -> linkifier().getRelativePath(Tools5eIndexType.itemProperty);\n            default -> null;\n        };\n\n        if (path == null || !index.customContentIncluded()) {\n            return linkText;\n        }\n\n        return \"[%s](%s%s.md)\".formatted(linkText, index.rulesVaultRoot(), path);\n    }\n\n    /**\n     * Get alias or default, with fallback for missing homebrew source\n     *\n     * @param type Type of resource\n     * @param name Name of resource (from tag)\n     * @param source Source of resource (from tag); may be null\n     * @return Key for resource (original, alias, or homebrew fallback)\n     */\n    default String findKey(Tools5eIndexType type, String name, String source) {\n        String key = index().getAliasOrDefault(type.createKey(name, source));\n        JsonNode targetNode = index().getOrigin(key); // test for existence\n        if (targetNode == null\n                && getSources().isHomebrew()\n                && source.equals(type.defaultSourceString())) {\n            // the variant rule wasn't found using the default source, but it is homebrew,\n            // so we'll try again with the homebrew source\n            String homebrewKey = index().getAliasOrDefault(type.createKey(name, getSources().primarySource()));\n            targetNode = index().getOrigin(homebrewKey);\n            if (targetNode != null) {\n                key = homebrewKey;\n            }\n        }\n        return key;\n    }\n\n    default String linkifyItemAttribute(Tools5eIndexType type, String s) {\n        String[] parts = s.split(\"\\\\|\");\n        String name = parts[0];\n        String linkText = valueOrDefault(parts, 2, name);\n        return switch (type) {\n            case itemMastery -> {\n                ItemMastery mastery = index().findItemMastery(s, getSources());\n                yield mastery == null\n                        ? linkText\n                        : mastery.linkify(linkText);\n            }\n            case itemProperty -> {\n                ItemProperty property = index().findItemProperty(s, getSources());\n                yield property == null\n                        ? linkText\n                        : property.linkify(linkText);\n            }\n            case itemType -> {\n                ItemType itemType = index().findItemType(s, getSources());\n                yield itemType == null\n                        ? linkText\n                        : itemType.linkify(linkText);\n            }\n            default -> linkText;\n        };\n    }\n\n    default String handleCitation(String citationTag) {\n        // Casting Times|FleeMortals|A\n        // Casting Times|FleeMortals|{@sup A}\n        String[] parts = citationTag.split(\"\\\\|\");\n        if (parts.length < 3) {\n            tui().errorf(\"Badly formed citation %s in %s\", citationTag, getSources().getKey());\n            return citationTag;\n        }\n        String key = index().getAliasOrDefault(Tools5eIndexType.citation.createKey(parts[0], parts[1]));\n        String annotation = replaceText(parts[2]).replaceAll(\"</?sup>\", \"\");\n        JsonNode jsonSource = index().getNode(key);\n        if (index().isExcluded(key) || jsonSource == null) {\n            return annotation;\n        }\n        String blockRef = \"^\" + slugify(key);\n        List<String> text = new ArrayList<>();\n        appendToText(text, jsonSource, null);\n        if (text.get(text.size() - 1).startsWith(\"^\")) {\n            blockRef = text.get(text.size() - 1);\n        } else {\n            text.add(blockRef);\n        }\n        parseState().addCitation(key, String.join(\"\\n\", text));\n        return \"[%s](#%s)\".formatted(annotation, blockRef);\n    }\n\n    default String decoratedUaName(String name, Tools5eSources sources) {\n        Optional<String> uaSource = sources.uaSource();\n        if (uaSource.isPresent() && !name.contains(\"(UA\")) {\n            return name + \" (\" + uaSource.get() + \")\";\n        }\n        return name;\n    }\n\n    default double convertToNumber(String text) {\n        if (text == null || text.isEmpty()) {\n            return 0;\n        }\n        Matcher m = FRACTIONAL.matcher(text);\n        if (m.matches()) {\n            double out = Double.parseDouble(m.group(1));\n            if (m.group(2) != null) {\n                switch (m.group(2)) {\n                    case \"⅛\":\n                        out += 0.125;\n                        break;\n                    case \"¼\":\n                        out += 0.25;\n                        break;\n                    case \"⅜\":\n                        out += 0.375;\n                        break;\n                    case \"½\":\n                        out += 0.5;\n                        break;\n                    case \"⅝\":\n                        out += 0.625;\n                        break;\n                    case \"¾\":\n                        out += 0.75;\n                        break;\n                    case \"⅞\":\n                        out += 0.875;\n                        break;\n                    case \"⅓\":\n                        out += 1 / 3;\n                        break;\n                    case \"⅔\":\n                        out += 2 / 3;\n                        break;\n                    case \"⅙\":\n                        out += 1 / 6;\n                        break;\n                    case \"⅚\":\n                        out += 5 / 6;\n                        break;\n                    case \"\":\n                        break;\n                }\n            }\n            return out;\n        }\n        throw new IllegalArgumentException(\"Unable to convert \" + text + \" to number\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/MagicVariant.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static dev.ebullient.convert.StringUtil.toTitleCase;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map.Entry;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\nimport java.util.regex.Pattern;\nimport java.util.stream.Stream;\n\nimport com.ezylang.evalex.EvaluationException;\nimport com.ezylang.evalex.Expression;\nimport com.ezylang.evalex.data.EvaluationValue;\nimport com.ezylang.evalex.parser.ParseException;\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.ArrayNode;\nimport com.fasterxml.jackson.databind.node.DoubleNode;\nimport com.fasterxml.jackson.databind.node.IntNode;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\n\nimport dev.ebullient.convert.io.Msg;\nimport dev.ebullient.convert.tools.JsonNodeReader;\nimport dev.ebullient.convert.tools.ToolsIndex.TtrpgValue;\nimport dev.ebullient.convert.tools.dnd5e.Json2QuteItem.ItemField;\nimport dev.ebullient.convert.tools.dnd5e.Tools5eSources.SourceAttributes;\n\npublic class MagicVariant implements JsonSource {\n\n    static final List<String> IGNORE = List.of(\"entries\", \"rarity\", \"namePrefix\", \"nameSuffix\");\n    static final Pattern EXPRESSION = Pattern.compile(\"\\\\[\\\\[([^\\\\]]+)]]\");\n    static final Pattern TEMPLATE = Pattern.compile(\"\\\\{=([^}]+)}\");\n\n    // Process operations in this order always (see Renderer.applyProperties._OP_ORDER)\n    static final String FIXED_OP_ORDER = \"ltua\";\n    static final List<String> _LEADING_AN = List.of(\"a\", \"e\", \"i\", \"o\", \"u\");\n\n    static final MagicVariant INSTANCE = new MagicVariant();\n\n    public static List<JsonNode> findSpecificVariants(Tools5eIndex index, Tools5eIndexType type,\n            String key, JsonNode genericVariant, Tools5eJsonSourceCopier copier,\n            List<JsonNode> baseItems) {\n        return INSTANCE.findVariants(index, type, key, genericVariant, copier, baseItems);\n    }\n\n    public static void populateGenericVariant(final JsonNode variant) {\n        INSTANCE.populateVariant(variant);\n    }\n\n    /**\n     * Update generic variant item with inherited attributes\n     * (minimally source, which is required to create the key)\n     */\n    private void populateVariant(final JsonNode variant) {\n        JsonNode inherits = MagicItemField.inherits.getFrom(variant);\n\n        // for (const prop in genericVariant.inherits) {\n        //     if (Renderer.item._INHERITED_PROPS_BLOCKLIST.has(prop)) continue;\n        //     const val = genericVariant.inherits[prop];\n        //     if (val == null) delete genericVariant[prop];\n        //     else if (genericVariant[prop]) {\n        //         if (genericVariant[prop] instanceof Array && val instanceof Array)\n        //             genericVariant[prop] = MiscUtil.copyFast(genericVariant[prop]).concat(val);\n        //         else genericVariant[prop] = val;\n        //     } else genericVariant[prop] = genericVariant.inherits[prop];\n        // }\n        List<String> fieldNames = streamOfFieldNames(inherits).toList();\n        for (String fieldName : fieldNames) {\n            if (IGNORE.contains(fieldName)) {\n                continue;\n            }\n            JsonNode existing = variant.get(fieldName);\n            JsonNode newValue = inherits.get(fieldName);\n            if ((newValue == null || newValue.isNull()) && existing != null) {\n                ((ObjectNode) variant).remove(fieldName);\n            } else if (existing != null && existing.isArray() && newValue.isArray()) {\n                ((ArrayNode) existing).addAll((ArrayNode) newValue);\n            } else {\n                ((ObjectNode) variant).set(fieldName, newValue);\n            }\n        }\n\n        // if (!genericVariant.entries && genericVariant.inherits.entries) {\n        //      genericVariant.entries = MiscUtil.copyFast(Renderer.applyAllProperties(genericVariant.inherits.entries, genericVariant.inherits));\n        // }\n        if (!SourceField.entries.existsIn(variant) && SourceField.entries.existsIn(inherits)) {\n            SourceField.entries.setIn(variant,\n                    resolveEntryAttributes(\n                            SourceField.entries.getFrom(inherits),\n                            tokenResolver(inherits)));\n        }\n\n        // if (genericVariant.inherits.rarity == null) delete genericVariant.rarity;\n        // else if (genericVariant.inherits.rarity === \"varies\") {\n        // /* No-op, i.e., use current rarity */\n        // } else genericVariant.rarity = genericVariant.inherits.rarity;\n        String value = MagicItemField.rarity.getTextOrNull(inherits);\n        if (value == null) {\n            MagicItemField.rarity.removeFrom(variant);\n        } else if (\"varies\".equals(value)) {\n            // no-op\n        } else {\n            MagicItemField.rarity.setIn(variant, value);\n        }\n\n        // if (genericVariant.requires.armor)\n        //     genericVariant.armor = genericVariant.requires.armor;\n        JsonNode armor = MagicItemField.armor.getFrom(MagicItemField.requires.getFrom(variant));\n        if (armor != null) {\n            MagicItemField.armor.setIn(variant, armor);\n        }\n    }\n\n    /** Update / replace item with variants (where appropriate) */\n    private List<JsonNode> findVariants(Tools5eIndex index, Tools5eIndexType type,\n            String key, JsonNode genericVariant, Tools5eJsonSourceCopier copier,\n            List<JsonNode> baseItems) {\n        List<JsonNode> variants = new ArrayList<>();\n        // baseItems.forEach((curBaseItem) => {\n        //     ....\n        //     genericVariants.forEach((curGenericVariant) => {\n        //         if (!Renderer.item._createSpecificVariants_isEditionMatch({curBaseItem, curGenericVariant})) return;\n        //         if (!Renderer.item._createSpecificVariants_hasRequiredProperty(curBaseItem, curGenericVariant)) return;\n        //         if (Renderer.item._createSpecificVariants_hasExcludedProperty(curBaseItem, curGenericVariant)) return;\n        //         genericAndSpecificVariants.push(Renderer.item._createSpecificVariants_createSpecificVariant(curBaseItem, curGenericVariant, opts));\n        //     });\n        // });\n        // ..\n        // We're looping the other way (variant is the outer loop / is passed in)\n        boolean spawnNewItems = key.contains(\" (*)\");\n\n        ArrayNode specificVariantListNode = null;\n        String gvKey = Tools5eIndexType.item.createKey(genericVariant);\n        if (!spawnNewItems) {\n            // Add generic variant to the list of variants as a regular item\n            // Variations will be added to this item.\n            TtrpgValue.indexInputType.setIn(genericVariant, Tools5eIndexType.item.name());\n            TtrpgValue.indexKey.setIn(genericVariant, gvKey);\n            variants.add(genericVariant);\n            index.addAlias(key, gvKey);\n            specificVariantListNode = ItemField._variants.ensureArrayIn(genericVariant);\n            ItemField._variants.setIn(genericVariant, specificVariantListNode);\n        }\n\n        String fluffKey = ItemField.hasFluff.booleanOrDefault(genericVariant, false)\n                || ItemField.hasFluffImages.booleanOrDefault(genericVariant, false)\n                        ? Tools5eIndexType.itemFluff.createKey(genericVariant)\n                        : null;\n        for (JsonNode baseItem : baseItems) {\n            if (ItemField.packContents.existsIn(baseItem)\n                    || !editionMatch(baseItem, genericVariant)\n                    || !hasRequiredProperty(baseItem, genericVariant)\n                    || hasExcludedProperty(baseItem, genericVariant)) {\n                continue;\n            }\n            JsonNode specficVariant = createSpecificVariant(baseItem, genericVariant);\n            if (specficVariant != null) {\n                String newKey = Tools5eIndexType.item.createKey(specficVariant);\n                TtrpgValue.indexInputType.setIn(specficVariant, Tools5eIndexType.item.name());\n                TtrpgValue.indexKey.setIn(specficVariant, newKey);\n                if (fluffKey != null) {\n                    TtrpgValue.indexFluffKey.setIn(specficVariant, fluffKey);\n                }\n                Tools5eSources variantSources = Tools5eSources.constructSources(newKey, specficVariant);\n                variantSources.amendHomebrewSources(baseItem);\n                variantSources.amendHomebrewSources(genericVariant);\n\n                if (spawnNewItems) {\n                    variants.add(specficVariant);\n                    if (key.replace(\" (*)\", \"\").replace(\"magicvariant\", \"item\").equals(newKey)) {\n                        index.addAlias(key, newKey);\n                    }\n                    if (gvKey.replace(\" (*)\", \"\").equals(newKey)) {\n                        index.addAlias(gvKey, newKey);\n                    }\n                } else {\n                    // add variant to list of variants for this generic variant\n                    // magic variant remains in index as a magic variant\n                    specificVariantListNode.add(specficVariant);\n                    index.addAlias(newKey, gvKey);\n                }\n            }\n        }\n        int numVariants = specificVariantListNode == null\n                ? variants.size()\n                : specificVariantListNode.size();\n        tui().logf(Msg.ITEM, \"Found %d specific variants for %s\", numVariants, key);\n        return variants;\n    }\n\n    // @formatter:off\n    /**\n     * render.js -- _createSpecificVariants_isEditionMatch\n     *\n     * When creating specific variants, the application of \"classic\" and \"one\" editions\n     * goes by the following logic:\n     *\n     * |  B. Item | Gen. Var | Apply | Example\n     * |----------|----------|-------|----------------------------------------\n     * |     null |     null |     X | \"Fool's Blade|BMT\" -> \"Pitchfork|ToB3-Lairs\"\n     * |  classic |     null |       | \"Fool's Blade|BMT\" -> \"Longsword|PHB\"\n     * |      one |     null |     X | \"Fool's Blade|BMT\" -> \"Longsword|XPHB\"\n     * |     null |  classic |     X | \"+1 Weapon|DMG\" -> \"Pitchfork|ToB3-Lairs\" -- TODO(Future): consider cutting this, with a homebrew tag migration\n     * |  classic |  classic |     X | \"+1 Weapon|DMG\" -> \"Longsword|PHB\"\n     * |      one |  classic |       | \"+1 Weapon|DMG\" -> \"Longsword|XPHB\"\n     * |     null |      one |     X | \"+1 Weapon|XDMG\" -> \"Pitchfork|ToB3-Lairs\"\n     * |  classic |      one |       | \"+1 Weapon|XDMG\" -> \"Longsword|PHB\"\n     * |      one |      one |     X | \"+1 Weapon|XDMG\" -> \"Longsword|XPHB\"\n     *\n     * This aims to minimize spamming near-duplicates, while preserving as many '14 items as possible.\n     */\n    // @formatter:on\n    boolean editionMatch(JsonNode baseItem, JsonNode genericVariant) {\n        String baseItemEdition = Tools5eFields.edition.getTextOrNull(baseItem);\n        String variantEdition = Tools5eFields.edition.getTextOrNull(genericVariant);\n        if (baseItemEdition == null && variantEdition == null) {\n            return true; // ok: null -> null\n        }\n        if (baseItemEdition != null) { // variantEdition may be null\n            if (baseItemEdition.equalsIgnoreCase(variantEdition)) {\n                return true; // ok: classic -> classic, one -> one\n            }\n            if (\"classic\".equalsIgnoreCase(baseItemEdition)) {\n                return false; // nope: classic -> one or classic -> null\n            }\n            if (\"one\".equalsIgnoreCase(baseItemEdition)) {\n                // ok: one -> null; nope: one -> classic\n                return !\"classic\".equalsIgnoreCase(variantEdition);\n            }\n        }\n        // ok: null -> classic, null -> one\n        return true;\n    }\n\n    /**\n     * render.js -- _createSpecificVariants_hasRequiredProperty\n     */\n    boolean hasRequiredProperty(JsonNode baseItem, JsonNode genericVariant) {\n        JsonNode variantRequires = MagicItemField.requires.getFrom(genericVariant);\n        if (variantRequires == null) {\n            return true; // all is well if there are no required properties defined\n        }\n        if (!variantRequires.isArray()) {\n            tui().errorf(\"Incorrect magic variant requirements: %s\", genericVariant);\n            return false;\n        }\n        // \"requires\": [\n        //   { \"weapon\": true },\n        //   { \"type\": \"S\" },\n        //   { \"net\": true }\n        // ],\n        // return genericVariant.requires.some(req =>\n        //      Renderer.item._createSpecificVariants_isRequiresExcludesMatch(baseItem, req, \"every\"));\n        return streamOf(variantRequires).anyMatch(req -> {\n            if (req != null && !req.isObject()) {\n                tui().errorf(\"Incorrectly specified magic variant requirement in %s: %s\",\n                        TtrpgValue.indexKey.getFrom(genericVariant), req);\n            }\n            return matchesRequiresExcludes(baseItem, req, true);\n        });\n    }\n\n    boolean hasExcludedProperty(JsonNode baseItem, JsonNode genericVariant) {\n        JsonNode excludes = MagicItemField.excludes.getFrom(genericVariant);\n        if (excludes != null && !excludes.isObject()) {\n            tui().errorf(\"Incorrectly specified magic variant excludes in %s: %s\",\n                    TtrpgValue.indexKey.getFrom(genericVariant), excludes);\n            return true;\n        }\n        // \"excludes\": {\n        //   \"net\": true\n        // },\n        // return Renderer.item._createSpecificVariants_isRequiresExcludesMatch(baseItem, genericVariant.excludes, \"some\");\n        return matchesRequiresExcludes(baseItem, excludes, false);\n    }\n\n    // _createSpecificVariants_isRequiresExcludesMatch\n    private boolean matchesRequiresExcludes(JsonNode candidate, JsonNode reqsOrExcludes, boolean matchAll) {\n        if (candidate == null || reqsOrExcludes == null) {\n            return false;\n        }\n\n        var entries = reqsOrExcludes.properties().stream();\n\n        return matchAll\n                ? entries.allMatch(e -> testProperty(candidate, e.getKey(), e.getValue(), matchAll))\n                : entries.anyMatch(e -> testProperty(candidate, e.getKey(), e.getValue(), matchAll));\n    }\n\n    private boolean testProperty(JsonNode candidate, String reqKey, JsonNode reqValue, boolean matchAll) {\n        JsonNode candidateValue = candidate.get(reqKey);\n        if (candidateValue == null || candidateValue.isNull()) {\n            return reqValue == null || reqValue.isNull();\n        }\n        if (reqValue.isArray()) {\n            return candidateValue.isArray()\n                    ? streamOf(candidateValue).anyMatch(it -> arrayContains(reqValue, it))\n                    : arrayContains(reqValue, candidateValue);\n        }\n        if (reqValue.isObject()) {\n            // recursion: chase required custom properties (e.g.)\n            return matchesRequiresExcludes(candidate.get(reqKey), reqValue, matchAll);\n        }\n        return candidateValue.isArray()\n                ? arrayContains(candidateValue, reqValue)\n                : reqValue.equals(candidateValue);\n    }\n\n    boolean arrayContains(JsonNode array, JsonNode value) {\n        return streamOf(array).anyMatch((x) -> x.equals(value));\n    }\n\n    private JsonNode resolveEntryAttributes(\n            JsonNode entriesNode,\n            BiFunction<String, Boolean, String> applyProperties) {\n        try {\n            String entriesTemplate = mapper().writeValueAsString(entriesNode);\n            entriesTemplate = replaceTokens(entriesTemplate, applyProperties);\n            return mapper().readTree(entriesTemplate);\n        } catch (JsonProcessingException e) {\n            tui().errorf(e, \"Unable to process entries from %s\", entriesNode);\n        }\n        return entriesNode;\n    }\n\n    private String processText(String text, Function<String, String> tokenResolver) {\n        if (text == null || text.isBlank()) {\n            return \"\";\n        }\n\n        // Handle {=thing|stuff} tokens\n        return TEMPLATE.matcher(text).replaceAll((match) -> {\n            String[] parts = match.group(1).trim().split(\"/\");\n            String key = parts[0];\n            String value = tokenResolver.apply(key);\n            if (parts.length > 1) {\n                List<String> modifiers = Stream.of(FIXED_OP_ORDER.split(\"\"))\n                        .filter(x -> parts[1].contains(x))\n                        .toList();\n                for (String m : modifiers) {\n                    value = switch (m) {\n                        case \"a\" -> {\n                            yield _LEADING_AN.contains(value.substring(0, 1).toLowerCase())\n                                    ? \"an\"\n                                    : \"a\";\n                        }\n                        case \"l\" -> value.toLowerCase();\n                        case \"t\" -> toTitleCase(value);\n                        case \"u\" -> value.toUpperCase();\n                        default -> {\n                            tui().errorf(\n                                    \"Unhandled modifier %s while processing %s (open an issue and include this message)\", m,\n                                    text);\n                            yield text;\n                        }\n                    };\n                }\n            }\n            return value;\n        });\n    }\n\n    private BiFunction<String, Boolean, String> tokenResolver(final JsonNode valueSource) {\n        return (s, b) -> {\n            return processText(s, (key) -> {\n                if (valueSource.has(key)) {\n                    return valueSource.get(key).asText();\n                }\n                tui().errorf(\"Replacement for %s not found in %s\", key, valueSource);\n                return key;\n            });\n        };\n    }\n\n    private BiFunction<String, Boolean, String> tokenResolver(final JsonNode baseItem, final JsonNode inherits) {\n        // _getInjectableProps (baseItem, inherits) {\n        //     return {\n        //         baseName: baseItem.name,\n        //         dmgType: baseItem.dmgType ? Parser.dmgTypeToFull(baseItem.dmgType) : null,\n        //         bonusAc: inherits.bonusAc,\n        //         bonusWeapon: inherits.bonusWeapon,\n        //         bonusWeaponAttack: inherits.bonusWeaponAttack,\n        //         bonusWeaponDamage: inherits.bonusWeaponDamage,\n        //         bonusWeaponCritDamage: inherits.bonusWeaponCritDamage,\n        //         bonusSpellAttack: inherits.bonusSpellAttack,\n        //         bonusSpellSaveDc: inherits.bonusSpellSaveDc,\n        //         bonusSavingThrow: inherits.bonusSavingThrow,\n        //     };\n        // },\n        return (s, b) -> {\n            return processText(s, (key) -> {\n                return switch (key) {\n                    case \"baseName\" -> SourceField.name.getTextOrEmpty(baseItem);\n                    case \"dmgType\" -> {\n                        JsonNode dmgType = MagicItemField.dmgType.getFrom(baseItem);\n                        if (dmgType != null) {\n                            yield damageTypeToFull(dmgType.asText());\n                        }\n                        yield \"\";\n                    }\n                    case \"bonusAc\" -> MagicItemField.bonusAc.getTextOrEmpty(inherits);\n                    case \"bonusWeapon\" -> MagicItemField.bonusWeapon.getTextOrEmpty(inherits);\n                    case \"bonusWeaponAttack\" -> MagicItemField.bonusWeaponAttack.getTextOrEmpty(inherits);\n                    case \"bonusWeaponDamage\" -> MagicItemField.bonusWeaponDamage.getTextOrEmpty(inherits);\n                    case \"bonusWeaponCritDamage\" -> MagicItemField.bonusWeaponCritDamage.getTextOrEmpty(inherits);\n                    case \"bonusSpellAttack\" -> MagicItemField.bonusSpellAttack.getTextOrEmpty(inherits);\n                    case \"bonusSpellSaveDc\" -> MagicItemField.bonusSpellSaveDc.getTextOrEmpty(inherits);\n                    case \"bonusSavingThrow\" -> MagicItemField.bonusSavingThrow.getTextOrEmpty(inherits);\n                    default -> key;\n                };\n            });\n        };\n    }\n\n    private JsonNode createSpecificVariant(JsonNode baseItem, JsonNode genericVariant) {\n        JsonNode inherits = MagicItemField.inherits.getFrom(genericVariant);\n        JsonNode specificVariant = copyNode(baseItem);\n\n        // Remove base item flag\n        TtrpgValue.indexBaseItem.removeFrom(specificVariant);\n\n        // Magic variants apply their own SRD info; page info\n        SourceAttributes.basicRules.removeFrom(specificVariant);\n        SourceAttributes.basicRules2024.removeFrom(specificVariant);\n        SourceAttributes.srd.removeFrom(specificVariant);\n        SourceAttributes.srd52.removeFrom(specificVariant);\n        SourceField.page.removeFrom(specificVariant);\n\n        // Magic items do not inherit the value of the non-magical item\n        ItemField.value.removeFrom(specificVariant);\n\n        // Reset or remove fluff specifiers based on generic variant\n        resetOrRemove(ItemField.hasFluff, genericVariant, specificVariant);\n        resetOrRemove(ItemField.hasFluffImages, genericVariant, specificVariant);\n\n        for (Entry<String, JsonNode> property : inherits.properties()) {\n            switch (property.getKey()) {\n                case \"barding\" -> {\n                    MagicItemField.bardingType.setIn(specificVariant, ItemField.type.getFrom(baseItem));\n                }\n                case \"entries\" -> {\n                    JsonNode entries = resolveEntryAttributes(property.getValue(),\n                            tokenResolver(baseItem, inherits));\n                    SourceField.entries.setIn(specificVariant, entries);\n                }\n                case \"namePrefix\" -> {\n                    String name = SourceField.name.getTextOrEmpty(specificVariant);\n                    SourceField.name.setIn(specificVariant, property.getValue().asText() + name);\n                }\n                case \"nameSuffix\" -> {\n                    String name = SourceField.name.getTextOrEmpty(specificVariant);\n                    SourceField.name.setIn(specificVariant, name + property.getValue().asText());\n                }\n                case \"nameRemove\" -> {\n                    Pattern p = Pattern.compile(property.getValue().asText());\n                    String name = SourceField.name.getTextOrEmpty(specificVariant);\n                    SourceField.name.setIn(specificVariant, p.matcher(name).replaceAll(\"\"));\n                }\n                case \"propertyAdd\" -> {\n                    ArrayNode itemProperty = ItemField.property.ensureArrayIn(specificVariant);\n                    index().copier.appendIfNotExistsArr(itemProperty, property.getValue());\n                }\n                case \"propertyRemove\" -> {\n                    ArrayNode itemProperty = ItemField.property.ensureArrayIn(specificVariant);\n                    index().copier.removeFromArr(itemProperty, property.getValue());\n                }\n                case \"valueExpression\", \"weightExpression\" -> {\n                    String expr = EXPRESSION.matcher(property.getValue().asText()).replaceAll((match) -> {\n                        JsonNode value = null;\n                        String[] path = match.group(1).split(\"\\\\.\");\n                        if (path[0].equalsIgnoreCase(\"baseitem\")) {\n                            value = baseItem.get(match.group(1).substring(9));\n                        } else if (path[0].equalsIgnoreCase(\"item\")) {\n                            value = specificVariant.get(match.group(1).substring(5));\n                        } else {\n                            value = specificVariant.get(match.group(1));\n                        }\n                        if (value == null) {\n                            return \"\";\n                        }\n                        return value.asText();\n                    });\n                    if (!expr.isBlank()) {\n                        try {\n                            Expression expression = new Expression(expr);\n                            EvaluationValue result = expression.evaluate();\n                            if (property.getKey() == \"valueExpression\") {\n                                IntNode value = IntNode.valueOf(result.getNumberValue().intValue());\n                                ItemField.value.setIn(specificVariant, value);\n                            } else {\n                                DoubleNode value = DoubleNode.valueOf(result.getNumberValue().doubleValue());\n                                ItemField.weight.setIn(specificVariant, value);\n                            }\n                        } catch (EvaluationException | ParseException e) {\n                            tui().errorf(e, \"Unable to parse %s: %s\", property.getKey(), property.getValue());\n                        }\n                    }\n                }\n                case \"conditionImmune\" -> {\n                    ArrayNode condImmune = MagicItemField.conditionImmune.ensureArrayIn(specificVariant);\n                    index().copier.appendIfNotExistsArr(condImmune, property.getValue());\n                }\n                case \"vulnerable\", \"resist\", \"immune\" -> {\n                    // TODO\n                    /* no-op */ }\n                default -> {\n                    ((ObjectNode) specificVariant).set(property.getKey(), copyNode(property.getValue()));\n                }\n            }\n        }\n        // TODO:\n        // Renderer.item._createSpecificVariants_mergeVulnerableResistImmune({specificVariant, inherits});\n\n        // Carry over any homebrew sources from the base or GV item\n        // so that any  properties or types can be rendered properly\n        if (TtrpgValue.homebrewSource.existsIn(genericVariant)) {\n            TtrpgValue.homebrewSource.copy(genericVariant, specificVariant);\n        }\n        return specificVariant;\n    }\n\n    private void resetOrRemove(JsonNodeReader field, JsonNode source, JsonNode target) {\n        JsonNode value = field.getFrom(source);\n        if (value == null || value.isNull()) {\n            field.removeFrom(target);\n        } else {\n            field.setIn(target, value);\n        }\n    }\n\n    enum MagicItemField implements JsonNodeReader {\n        armor,\n        barding,\n        bardingType,\n        conditionImmune,\n        customProperties,\n        entries,\n        excludes,\n        immune,\n        inherits,\n        namePrefix,\n        nameRemove,\n        nameSuffix,\n        packContents, // not specific variant\n        propertyAdd,\n        propertyRemove,\n        rarity,\n        requires,\n        resist,\n        valueExpression,\n        vulnerable,\n        weightExpression,\n        bonusAc,\n        dmgType,\n        damageType,\n        bonusWeapon,\n        bonusWeaponAttack,\n        bonusWeaponDamage,\n        bonusWeaponCritDamage,\n        bonusSpellAttack,\n        bonusSpellSaveDc,\n        bonusSavingThrow,\n    }\n\n    @Override\n    public Tools5eIndex index() {\n        return Tools5eIndex.instance();\n    }\n\n    @Override\n    public Tools5eSources getSources() {\n        // TODO Auto-generated method stub\n        throw new UnsupportedOperationException(\"Unimplemented method 'getSources'\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/OptionalFeatureIndex.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static dev.ebullient.convert.StringUtil.join;\nimport static dev.ebullient.convert.StringUtil.joinConjunct;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\n\nimport dev.ebullient.convert.config.CompendiumConfig;\nimport dev.ebullient.convert.io.Msg;\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.tools.JsonNodeReader;\nimport dev.ebullient.convert.tools.dnd5e.HomebrewIndex.HomebrewMetaTypes;\nimport dev.ebullient.convert.tools.dnd5e.Json2QuteClass.ClassFields;\nimport io.quarkus.runtime.annotations.RegisterForReflection;\n\n@RegisterForReflection\npublic class OptionalFeatureIndex implements JsonSource {\n    private final Map<String, OptionalFeatureType> optFeatureIndex = new HashMap<>();\n    private final Set<String> unresolvedFeatureTypes = new HashSet<>();\n    private final Tools5eIndex index;\n\n    OptionalFeatureIndex(Tools5eIndex index) {\n        this.index = index;\n    }\n\n    public OptionalFeatureType addOptionalFeatureType(String featureType, HomebrewMetaTypes homebrew) {\n        // scope the optional feature key (homebrew may conflict)\n        try {\n            var oft = optFeatureIndex.computeIfAbsent(featureType.toLowerCase(),\n                    k -> new OptionalFeatureType(featureType, homebrew, index()));\n            oft.addHomebrewMeta(homebrew);\n            return oft;\n        } catch (IllegalArgumentException e) {\n            tui().errorf(e, \"Unable to define optional feature\");\n        }\n        return null;\n    }\n\n    public void addOptionalFeature(String finalKey, JsonNode optFeatureNode, HomebrewMetaTypes homebrew) {\n        for (String ft : OftFields.featureType.getListOfStrings(optFeatureNode, tui())) {\n            var oft = addOptionalFeatureType(ft, homebrew);\n            if (oft != null) {\n                oft.addFeature(finalKey);\n            }\n        }\n    }\n\n    public void amendSources(String key, JsonNode jsonSource) {\n        Tools5eSources sources = Tools5eSources.findSources(key);\n        if (sources.getType() == Tools5eIndexType.optfeature) {\n            for (String featureType : OftFields.featureType.getListOfStrings(jsonSource, tui())) {\n                OptionalFeatureType oft = get(featureType);\n                if (oft == null) {\n                    tui().warnf(Msg.UNRESOLVED, \"OptionalFeatureType %s not found for %s\", jsonSource, key);\n                } else {\n                    oft.amendSources(sources);\n                }\n            }\n        } else {\n            for (JsonNode ofp : ClassFields.optionalfeatureProgression.iterateArrayFrom(jsonSource)) {\n                for (String featureType : Tools5eFields.featureType.getListOfStrings(ofp, tui())) {\n                    // class/subclass source matters for homebrew scope (if necessary)\n                    OptionalFeatureType oft = get(featureType);\n                    if (oft == null) {\n                        tui().warnf(Msg.UNRESOLVED, \"OptionalFeatureType %s not found for %s\",\n                                featureType, key);\n                        continue;\n                    }\n                    oft.addConsumer(key);\n                    oft.amendSources(sources);\n                }\n            }\n        }\n    }\n\n    public void removeUnusedOptionalFeatures(\n            Function<String, Boolean> testInUse,\n            Consumer<String> keep,\n            Consumer<String> remove) {\n        for (var oft : optFeatureIndex.values()) {\n            // Test to see if any of the features using this type are still active.\n            if (oft.testFeaturesInUse(testInUse) || oft.testConsumersInUse(testInUse)) {\n                keep.accept(oft.getKey());\n                continue;\n            }\n\n            // Remove the feature type\n            remove.accept(oft.getKey());\n            // Remove all features associated with this type\n            oft.features.forEach(remove);\n        }\n    }\n\n    public OptionalFeatureType get(JsonNode node) {\n        if (node == null) {\n            return null;\n        }\n        String lookup = SourceField.name.getTextOrEmpty(node);\n        return lookup == null ? null : optFeatureIndex.get(lookup.toLowerCase());\n    }\n\n    public OptionalFeatureType get(String featureType) {\n        var lowerType = featureType.toLowerCase();\n        OptionalFeatureType type = optFeatureIndex.get(lowerType);\n        if (type == null && unresolvedFeatureTypes.add(lowerType)) {\n            tui().logf(Msg.UNRESOLVED, \"OptionalFeatureType %s not found\", lowerType);\n        }\n        return type;\n    }\n\n    public void clear() {\n        optFeatureIndex.clear();\n    }\n\n    public Map<String, OptionalFeatureType> getMap() {\n        return optFeatureIndex;\n    }\n\n    public static class OptionalFeatureCondition {\n        final int order;\n        final String name;\n        final List<String> includes = new ArrayList<>();\n        final List<String> includeConditions = new ArrayList<>();\n\n        final List<String> excludes = new ArrayList<>();\n        final List<String> excludeConditions = new ArrayList<>();\n\n        public OptionalFeatureCondition(int order, String name,\n                List<String> conditions,\n                Function<String, String> transform) {\n            this.order = order;\n            this.name = name;\n\n            for (String condition : conditions) {\n                if (condition.startsWith(\"!\")) {\n                    String exclude = condition.substring(1);\n                    excludeConditions.add(exclude);\n                    excludes.add(transform.apply(exclude));\n                } else {\n                    includeConditions.add(condition);\n                    includes.add(transform.apply(condition));\n                }\n            }\n        }\n\n        public int order() {\n            return order;\n        }\n\n        public boolean isEmpty() {\n            return includes.isEmpty() && excludes.isEmpty();\n        }\n\n        @Override\n        public String toString() {\n            if (!includes.isEmpty() && !excludes.isEmpty()) {\n                return String.format(\"%s %s, excluding %s\", name,\n                        joinConjunct(\" or \", includes), join(\" and \", excludes));\n            } else if (!includes.isEmpty()) {\n                return String.format(\"%s %s\", name, joinConjunct(\" or \", includes));\n            } else if (!excludes.isEmpty()) {\n                return String.format(\"%s excluding %s\", name, joinConjunct(\" and \", includes));\n            }\n            return \"\";\n        }\n    }\n\n    /**\n     * This is included in all-index.json\n     */\n    static class OptionalFeatureType {\n\n        final String featureTypeKey;\n        final String abbreviation;\n        final List<String> features = new ArrayList<>();\n        final List<String> consumers = new ArrayList<>();\n\n        @JsonIgnore\n        final ObjectNode featureTypeNode;\n\n        @JsonIgnore\n        final Map<String, HomebrewMetaTypes> homebrewMeta = new HashMap<>();\n\n        Tools5eSources sources; // deferred initialization\n\n        OptionalFeatureType(String abbreviation, HomebrewMetaTypes homebrewMeta, Tools5eIndex index) {\n            this.abbreviation = abbreviation;\n            String primarySource = getSource(homebrewMeta);\n\n            featureTypeNode = Tui.MAPPER.createObjectNode();\n            featureTypeNode.put(\"name\", abbreviation);\n            featureTypeNode.put(\"source\", primarySource);\n\n            if (inSRD(abbreviation)) {\n                featureTypeNode.put(\"srd\", true);\n                featureTypeNode.put(\"srd52\", true);\n            }\n            // KNOCK-ON: Add to index\n            this.featureTypeKey = Tools5eIndexType.optionalFeatureTypes.createKey(featureTypeNode);\n            index.addToIndex(Tools5eIndexType.optionalFeatureTypes, featureTypeNode);\n            // wait to construct sources\n        }\n\n        public void amendSources(Tools5eSources otherSources) {\n            var mySources = mySources();\n            // Update sources from those of a consuming/using class or subclass\n            // Optional features will always add to sources of types\n            if (otherSources.getType() == Tools5eIndexType.optfeature\n                    || otherSources.contains(mySources)) {\n                mySources.amendSources(otherSources);\n            }\n        }\n\n        public void addHomebrewMeta(HomebrewMetaTypes homebrew) {\n            if (homebrew != null) {\n                homebrewMeta.put(homebrew.primary, homebrew);\n                mySources().amendSources(homebrew.sourceKeys);\n            }\n        }\n\n        private Tools5eSources mySources() {\n            if (this.sources == null) {\n                this.sources = Tools5eSources.constructSources(featureTypeKey, featureTypeNode);\n            }\n            return this.sources;\n        }\n\n        public void addConsumer(String key) {\n            consumers.add(key);\n        }\n\n        public void addFeature(String key) {\n            features.add(key);\n        }\n\n        public String getFilename() {\n            return linkifier().getOptionalFeatureTypeResource(abbreviation);\n        }\n\n        public Tools5eSources getSources() {\n            return Tools5eSources.findSources(featureTypeKey);\n        }\n\n        public boolean testConsumersInUse(Function<String, Boolean> test) {\n            return consumers.stream()\n                    .map(k -> test.apply(k))\n                    .reduce(Boolean::logicalOr)\n                    .orElse(false);\n        }\n\n        public boolean testFeaturesInUse(Function<String, Boolean> test) {\n            return features.stream()\n                    .map(k -> test.apply(k))\n                    .reduce(Boolean::logicalOr)\n                    .orElse(false);\n        }\n\n        public String getTitle() {\n            String title = JsonSource.featureTypeToString(abbreviation, homebrewMeta);\n            if (title.equalsIgnoreCase(abbreviation)) {\n                Tui.instance().warnf(Msg.NOT_SET, \"Missing title for OptionalFeatureType in %s\",\n                        abbreviation);\n                return abbreviation;\n            }\n            return title;\n        }\n\n        private String getSource(HomebrewMetaTypes homebrewMeta) {\n            return switch (abbreviation) {\n                case \"AF\" -> \"UAA\";\n                case \"AI\", \"RN\" -> \"TCE\";\n                case \"AS\", \"FS:B\" -> \"XGE\";\n                case \"AS:V1-UA\" -> \"UAF\";\n                case \"AS:V2-UA\" -> \"UARSC\";\n                case \"MV:C2-UA\" -> \"UARCO\";\n                case \"OR\" -> \"UACDW\";\n                case \"TT\" -> \"HWCS\";\n                default -> {\n                    if (homebrewMeta != null) {\n                        yield homebrewMeta.primary;\n                    }\n                    yield \"PHB\";\n                }\n            };\n        }\n\n        private boolean inSRD(String abbreviation) {\n            return switch (abbreviation) {\n                case \"EI\", \"FS:F\", \"FS:R\", \"FS:P\", \"MM\", \"PB\" -> true;\n                default -> false;\n            };\n        }\n\n        @JsonIgnore\n        String getKey() {\n            return featureTypeKey;\n        }\n\n        Tools5eLinkifier linkifier() {\n            return Tools5eLinkifier.instance();\n        }\n    }\n\n    @Override\n    public CompendiumConfig cfg() {\n        return index.config;\n    }\n\n    @Override\n    public Tools5eIndex index() {\n        return index;\n    }\n\n    @Override\n    public Tools5eSources getSources() {\n        return null;\n    }\n\n    enum OftFields implements JsonNodeReader {\n        featureType,\n        optionalFeatureTypes,\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/PsionicType.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport io.quarkus.runtime.annotations.RegisterForReflection;\n\npublic interface PsionicType {\n\n    default String combineWith(String order) {\n        if (order == null || order.isEmpty()) {\n            return getFullName();\n        }\n\n        return isAltDisplay()\n                ? getFullName() + \" (\" + order + \")\"\n                : order + \" \" + getFullName();\n    }\n\n    String getFullName();\n\n    boolean isAltDisplay();\n\n    @RegisterForReflection\n    static class CustomPsionicType implements PsionicType {\n        @JsonProperty(\"full\")\n        String fullName;\n        @JsonProperty(\"short\")\n        String shortName;\n        boolean hasOrder;\n        boolean isAltDisplay;\n\n        public String getFullName() {\n            return fullName;\n        }\n\n        public boolean isAltDisplay() {\n            return isAltDisplay;\n        }\n    }\n\n    enum PsionicTypeEnum implements PsionicType {\n\n        Discipline(\"D\"),\n        Talent(\"T\");\n\n        private String shortName;\n\n        PsionicTypeEnum(String shortName) {\n            this.shortName = shortName;\n        }\n\n        public String getFullName() {\n            return name();\n        }\n\n        public String getShortName() {\n            return shortName;\n        }\n\n        public boolean isAltDisplay() {\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/SkillOrAbility.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static dev.ebullient.convert.StringUtil.asModifier;\nimport static dev.ebullient.convert.StringUtil.isPresent;\nimport static dev.ebullient.convert.StringUtil.joinConjunct;\nimport static dev.ebullient.convert.StringUtil.toTitleCase;\n\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.tools.JsonNodeReader;\nimport dev.ebullient.convert.tools.JsonTextConverter.SourceField;\n\npublic interface SkillOrAbility {\n    static final Comparator<SkillOrAbility> comparator = Comparator.comparingInt(SkillOrAbility::ordinal)\n            .thenComparing(Comparator.comparing(SkillOrAbility::value));\n    static final CustomSkillOrAbility special = new CustomSkillOrAbility(\"Special\");\n\n    String value();\n\n    String source();\n\n    int ordinal();\n\n    public static SkillOrAbility fromTextValue(String v) {\n        if (v == null || v.isBlank()) {\n            return SkillOrAbilityEnum.None;\n        }\n        String lower = v.toLowerCase().replace(\" saving throws\", \"\").replace(\"_\", \"\");\n        if (\"special\".equals(lower)) {\n            return special;\n        }\n        for (SkillOrAbilityEnum s : SkillOrAbilityEnum.values()) {\n            if (s.lowerValue.equals(lower) || s.name().toLowerCase().equals(lower)) {\n                return s;\n            }\n        }\n        return null;\n    }\n\n    public static final List<String> allSkills = Stream.of(SkillOrAbilityEnum.values())\n            .filter(x -> x.isSkill)\n            .map(x -> x.longValue)\n            .collect(Collectors.toList());\n\n    public static final List<String> allSaves = Stream.of(SkillOrAbilityEnum.values())\n            .filter(x -> !x.isSkill)\n            .map(x -> x.longValue)\n            .collect(Collectors.toList());\n\n    public static String format(String key, Tools5eIndex index, Tools5eSources sources) {\n        SkillOrAbility skill = index.findSkillOrAbility(key, sources);\n        return skill == null ? key : skill.value();\n    }\n\n    public class CustomSkillOrAbility implements SkillOrAbility {\n        final String name;\n        final String lower;\n        final String key;\n        final String source;\n\n        public CustomSkillOrAbility(String name) {\n            this.name = name;\n            this.lower = name.toLowerCase();\n            this.key = null;\n            this.source = \"\";\n        }\n\n        public CustomSkillOrAbility(JsonNode skill) {\n            this.name = toTitleCase(SourceField.name.getTextOrEmpty(skill));\n            this.lower = this.name.toLowerCase();\n            this.key = Tools5eIndexType.skill.createKey(skill);\n            this.source = SourceField.source.getTextOrEmpty(skill);\n        }\n\n        @Override\n        public String value() {\n            return name;\n        }\n\n        @Override\n        public String source() {\n            return source;\n        }\n\n        public int ordinal() {\n            return 99;\n        }\n    }\n\n    enum SkillOrAbilityEnum implements SkillOrAbility {\n        STR(\"Strength\", false),\n        DEX(\"Dexterity\", false),\n        CON(\"Constitution\", false),\n        INT(\"Intelligence\", false),\n        WIS(\"Wisdom\", false),\n        CHA(\"Charisma\", false),\n\n        Acrobatics(\"Acrobatics\", true),\n        AnimalHandling(\"Animal Handling\", true),\n        Arcana(\"Arcana\", true),\n        Athletics(\"Athletics\", true),\n        Deception(\"Deception\", true),\n        History(\"History\", true),\n        Insight(\"Insight\", true),\n        Intimidation(\"Intimidation\", true),\n        Investigation(\"Investigation\", true),\n        Medicine(\"Medicine\", true),\n        Nature(\"Nature\", true),\n        Perception(\"Perception\", true),\n        Performance(\"Performance\", true),\n        Persuasion(\"Persuasion\", true),\n        Religion(\"Religion\", true),\n        SleightOfHand(\"Sleight of Hand\", true),\n        Stealth(\"Stealth\", true),\n        Survival(\"Survival\", true),\n        Any(\"Any\", false),\n        Varies(\"Varies\", false),\n        SidekickSpellcasting(\"Spellcasting\", true),\n        None(\"None\", false);\n\n        private final String longValue;\n        private final String lowerValue;\n        private final boolean isSkill;\n\n        SkillOrAbilityEnum(String longValue, boolean isSkill) {\n            this.longValue = longValue;\n            this.lowerValue = longValue.toLowerCase();\n            this.isSkill = isSkill;\n        }\n\n        public String value() {\n            return longValue;\n        }\n\n        public String source() {\n            return Tools5eIndexType.skill.defaultSourceString();\n        }\n    }\n\n    /** Ability scores related to race/species */\n    public static String getAbilityScore(JsonNode abilityScore) {\n        return processAbilityScoreArray(abilityScore, false);\n    }\n\n    /** Ability score increases for feats and backgrounds */\n    public static String getAbilityScoreIncreases(JsonNode abilityScores) {\n        return processAbilityScoreArray(abilityScores, true);\n    }\n\n    /** Single ability score increase (feat entries) */\n    public static String getAbilityScoreIncrease(JsonNode abilityScores) {\n        return abilityScore(abilityScores, true);\n    }\n\n    private static String processAbilityScoreArray(JsonNode abilityScores, boolean increase) {\n        if (abilityScores == null || !abilityScores.isArray()) {\n            return null;\n        }\n\n        List<String> list = new ArrayList<>();\n        for (JsonNode abilityNode : abilityScores) {\n            String result = abilityScore(abilityNode, increase);\n            if (isPresent(result)) {\n                list.add(result);\n            }\n        }\n        return String.join(\"; \", list);\n    }\n\n    private static String abilityScore(JsonNode abilityNode, boolean increase) {\n        var transform = Tools5eIndex.instance();\n        String max = \"\" + AsiFields.max.intOrDefault(abilityNode, 20);\n\n        JsonNode choose = AsiFields.choose.getFrom(abilityNode);\n        String result = \"\";\n        if (choose == null) {\n            result = transform.streamOfFieldNames(abilityNode)\n                    .filter(n -> !n.equalsIgnoreCase(\"max\"))\n                    .map(n -> toAbilityString(n, abilityNode.get(n), increase))\n                    .collect(Collectors.joining(\" \"));\n        } else if (AsiChoiceFields.weighted.existsIn(choose)) {\n            result = AsiChoiceFields.weighted.readWeightedChoice(choose);\n        } else if (AsiChoiceFields.from.existsIn(choose)) {\n            result = AsiChoiceFields.from.readFromChoice(choose, increase);\n        }\n        return result.replace(\"{@MAX}\", max);\n    }\n\n    private static String toAbilityString(String nameAbv, JsonNode value, boolean increase) {\n        if (increase) {\n            SkillOrAbility ability = SkillOrAbility.fromTextValue(nameAbv);\n            return \"Increase your %s score by %s, to a maximum of {@MAX}.\".formatted(\n                    ability == null ? toTitleCase(nameAbv) : ability.value(),\n                    value.asText());\n        }\n        return \"%s %s\".formatted(nameAbv, toModifier(value));\n    }\n\n    private static String toModifier(JsonNode value) {\n        try {\n            int v = Integer.parseInt(value.toString());\n            return asModifier(v);\n        } catch (NumberFormatException e) {\n            return value.toString();\n        }\n    }\n\n    public enum AsiChoiceFields implements JsonNodeReader {\n        from,\n\n        amount,\n        count,\n        entry,\n\n        weighted,\n        weights,\n\n        unknown // catcher for unknown attributes (see #fromString())\n        ;\n\n        // _mergeAbilityIncrease_getText\n        private String readFromChoice(JsonNode choiceNode, boolean increase) {\n            String entry = AsiChoiceFields.entry.replaceTextFrom(choiceNode, Tools5eIndex.instance());\n            if (isPresent(entry)) {\n                return entry;\n            }\n            JsonNode fromNode = getFrom(choiceNode);\n            if (fromNode == null) {\n                return null;\n            }\n            int count = AsiChoiceFields.count.intOrDefault(fromNode, 1);\n            int amount = AsiChoiceFields.amount.intOrDefault(fromNode, 1);\n            List<String> options = optionsFrom(fromNode);\n\n            if (increase) {\n                return options.size() == 6\n                        ? \"Increase one ability score of your choice by %s, to a maximum of {@MAX}.\"\n                                .formatted(amount)\n                        : \"Increase your %s by %s, to a maximum of {@MAX}.\"\n                                .formatted(joinConjunct(\", \", \" or \", options), amount);\n            }\n\n            return \"Apply %s to %s of %s.\"\n                    .formatted(asModifier(amount), count == 1 ? \"one\" : count + \" (distinct)\",\n                            joinConjunct(\", \", \" or \", options));\n        }\n\n        // _mergeAbilityIncrease_getText\n        private String readWeightedChoice(JsonNode choiceNode) {\n            JsonNode weightedNode = getFrom(choiceNode);\n            if (weightedNode == null) {\n                return null;\n            }\n            JsonNode weights = AsiChoiceFields.weights.getFrom(weightedNode);\n            if (weights == null) {\n                return null;\n            }\n\n            List<String> adjustments = new ArrayList<>();\n            for (int i = 0; i < weights.size(); i++) {\n                int adj = weights.get(i).asInt();\n                adjustments.add(\"%s ability score to %s by %s\".formatted(\n                        i == 0 ? \"an\" : \"another\",\n                        adj > 0 ? \"increase\" : \"decrease\",\n                        Math.abs(adj)));\n            }\n            String allAdjustments = joinConjunct(\", \", \" and \", adjustments);\n\n            List<String> options = optionsFrom(AsiChoiceFields.from.getFrom(weightedNode));\n            if (options.size() == 6) {\n                return \"Choose %s.\".formatted(allAdjustments);\n            }\n\n            return \"Choose %s from among %s.\".formatted(\n                    allAdjustments,\n                    joinConjunct(\", \", \" and \", options));\n        }\n\n        private static List<String> optionsFrom(JsonNode fromNode) {\n            if (fromNode == null) {\n                return null;\n            }\n            List<String> options = new ArrayList<>();\n            for (JsonNode option : fromNode) {\n                options.add(AsiFields.asiFieldFromString(option.asText()).longName());\n            }\n            return options;\n        }\n    }\n\n    public enum AsiFields implements JsonNodeReader {\n        str(SkillOrAbilityEnum.STR),\n        dex(SkillOrAbilityEnum.DEX),\n        con(SkillOrAbilityEnum.CON),\n        intel(SkillOrAbilityEnum.INT, \"int\"),\n        wis(SkillOrAbilityEnum.WIS),\n        cha(SkillOrAbilityEnum.CHA),\n        choose,\n        hidden, // ignored\n        max,\n        unknown // catcher for unknown attributes (see #fromString())\n        ;\n\n        private final SkillOrAbilityEnum ability;\n        private final String altName;\n\n        AsiFields() {\n            this(null, null);\n        }\n\n        AsiFields(SkillOrAbilityEnum ability) {\n            this(ability, null);\n        }\n\n        AsiFields(SkillOrAbilityEnum ability, String altName) {\n            this.ability = ability;\n            this.altName = altName;\n        }\n\n        public String nodeName() {\n            return altName == null ? name() : altName;\n        }\n\n        public SkillOrAbilityEnum getAbility() {\n            return ability;\n        }\n\n        public String longName() {\n            return ability == null ? \"Choose\" : ability.value();\n        }\n\n        private static AsiFields asiFieldFromString(String name) {\n            for (AsiFields field : AsiFields.values()) {\n                if (field.name().equalsIgnoreCase(name) || field.nodeName().equalsIgnoreCase(name)) {\n                    return field;\n                }\n            }\n\n            return AsiFields.unknown;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/SpellEntry.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static dev.ebullient.convert.StringUtil.isPresent;\nimport static dev.ebullient.convert.StringUtil.join;\n\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.Set;\nimport java.util.TreeMap;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.io.Msg;\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.tools.JsonTextConverter.SourceField;\nimport dev.ebullient.convert.tools.dnd5e.SpellIndex.SpellIndexFields;\n\npublic class SpellEntry {\n    final String level;\n    final String spellKey;\n    final Map<String, SpellReference> references = new TreeMap<>();\n    final Map<String, SpellReference> expandedList = new TreeMap<>();\n    final Set<String> classes = new HashSet<>();\n    final Set<String> classExpanded = new HashSet<>();\n    final SpellSchool school;\n    final boolean ritual;\n    final List<String> components;\n    final List<String> spellAttack;\n\n    @JsonIgnore\n    final JsonNode spellNode;\n\n    /**\n     * Created as spells are read from index\n     * Needed for filtering spells for indexes\n     */\n    public SpellEntry(String key, JsonNode spellNode) {\n        this.spellKey = key;\n        this.spellNode = spellNode;\n        this.level = SpellIndexFields.level.getTextOrEmpty(spellNode);\n        this.ritual = spellIsRitual(spellNode);\n        this.components = spellComponents(spellNode);\n        this.spellAttack = spellAttack(spellNode);\n        this.school = spellSchool(spellNode);\n    }\n\n    public String getLevelText() {\n        return JsonSource.spellLevelToText(level);\n    }\n\n    public String getLevel() {\n        return level;\n    }\n\n    public String getName() {\n        return linkifier().decoratedName(Tools5eIndexType.spell, spellNode);\n    }\n\n    /**\n     * Added when reading legacy spell definitions from the spell node\n     * Considered an expansion if the variantNode is present\n     * (i.e. TCE expands the spell list for ranger... )\n     */\n    public SpellReference addSpellReference(String refererKey, boolean expanded) {\n        SpellReference ref = new SpellReference(refererKey, expanded);\n        if (expanded) {\n            expandedList.put(refererKey, ref);\n        } else {\n            references.put(refererKey, ref);\n        }\n        if (ref.refererType == Tools5eIndexType.classtype) {\n            // Create class index without source (Wizard) for filters -> spellEntry\n            String className = SourceField.name.getTextOrEmpty(ref.refererNode).toLowerCase();\n            classes.add(className);\n            if (expanded) {\n                classExpanded.add(className);\n            }\n        }\n        return ref;\n    }\n\n    public SpellReference addReference(String refererKey, String constraint, String asLevel, boolean expanded,\n            String groupName) {\n        SpellReference ref = new SpellReference(refererKey, constraint, asLevel, expanded, groupName);\n        return addReference(ref);\n    }\n\n    /**\n     * Add a reference to this spell from the `additionalSpells` attribute.\n     * There is more information here: is there a class-level requirement, or\n     * a spell-slot level requirement; does this expand the class spell list; etc.\n     * Given these have more detail (and are newer), they may replace a more basic\n     * reference.\n     *\n     * @param ref\n     */\n    public SpellReference addReference(SpellReference ref) {\n        return addOrReplace(ref, ref.expanded ? expandedList : references);\n    }\n\n    private SpellReference addOrReplace(SpellReference spellRef, Map<String, SpellReference> set) {\n        return set.compute(spellRef.refererKey, (k, existingRef) -> {\n            if (existingRef != null && existingRef.isSpecific()) {\n                return existingRef; // Keep the existing specific reference\n            }\n            return spellRef; // Add new or replace non-specific\n        });\n    }\n\n    private boolean spellIsRitual(JsonNode spellNode) {\n        boolean ritual = false;\n        JsonNode meta = SpellIndexFields.meta.getFrom(spellNode);\n        if (meta != null) {\n            ritual = SpellIndexFields.ritual.booleanOrDefault(meta, false);\n        }\n        return ritual;\n    }\n\n    private List<String> spellComponents(JsonNode spellNode) {\n        JsonSource converter = Tools5eIndex.instance();\n        List<String> list = new ArrayList<>();\n        for (Entry<String, JsonNode> f : SpellIndexFields.components.iterateFieldsFrom(spellNode)) {\n            switch (f.getKey().toLowerCase()) {\n                case \"v\" -> list.add(\"V\");\n                case \"s\" -> list.add(\"S\");\n                case \"m\" -> {\n                    list.add(materialComponents(f.getValue(), converter));\n                }\n                case \"r\" -> list.add(\"R\"); // Royalty. Acquisitions Incorporated\n            }\n        }\n        return list;\n    }\n\n    String materialComponents(JsonNode source, JsonSource converter) {\n        return \"M (%s)\".formatted(\n                source.isObject()\n                        ? SpellIndexFields.text.replaceTextFrom(source, converter)\n                        : converter.replaceText(source));\n    }\n\n    private List<String> spellAttack(JsonNode spellNode) {\n        List<String> list = new ArrayList<>();\n        for (Entry<String, JsonNode> f : SpellIndexFields.spellAttack.iterateFieldsFrom(spellNode)) {\n            switch (f.getKey().toLowerCase()) {\n                case \"m\" -> list.add(\"M\"); // melee\n                case \"r\" -> list.add(\"R\"); // ranged\n                case \"o\" -> list.add(\"O\"); // other/unknown\n                default -> {\n                }\n            }\n        }\n        return list;\n    }\n\n    private SpellSchool spellSchool(JsonNode spellNode) {\n        String school = SpellIndexFields.school.getTextOrEmpty(spellNode);\n        return Tools5eIndex.instance().findSpellSchool(school, Tools5eSources.findSources(spellNode));\n    }\n\n    public SpellReference getReference(String key) {\n        SpellReference ref = references.get(key);\n        if (ref == null) {\n            return expandedList.get(key);\n        }\n        return ref;\n    }\n\n    /**\n     * Returns the most specific reference for the given key, preferring specific\n     * (constraint-bearing) references over non-specific ones. Checks expandedList\n     * first, then references, returning whichever has a constraint (classLevel/spellLevel/asLevel).\n     */\n    public SpellReference getMostSpecificReference(String key) {\n        SpellReference expandedRef = expandedList.get(key);\n        if (expandedRef != null && expandedRef.isSpecific()) {\n            return expandedRef;\n        }\n        SpellReference ref = references.get(key);\n        if (ref != null && ref.isSpecific()) {\n            return ref;\n        }\n        return ref != null ? ref : expandedRef;\n    }\n\n    public boolean inClassList(String x) {\n        return classes.contains(x.toLowerCase());\n    }\n\n    public boolean isExpanded(String className) {\n        return classExpanded.contains(className.toLowerCase());\n    }\n\n    public String linkify() {\n        Tools5eSources sources = Tools5eSources.findSources(spellNode);\n        return linkifier().linkSpellEntry(sources);\n    }\n\n    @Override\n    public String toString() {\n        return \"spellEntry[\" + spellKey + \";l=\" + level + \";classes=\" + classes + \";classExp=\" + classExpanded + \"]\";\n    }\n\n    @Override\n    public int hashCode() {\n        final int prime = 31;\n        int result = 1;\n        result = prime * result + ((spellKey == null) ? 0 : spellKey.hashCode());\n        return result;\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n        if (this == obj) {\n            return true;\n        }\n        if (obj == null) {\n            return false;\n        }\n        if (getClass() != obj.getClass()) {\n            return false;\n        }\n        SpellEntry other = (SpellEntry) obj;\n        if (spellKey == null) {\n            if (other.spellKey != null) {\n                return false;\n            }\n        } else if (!spellKey.equals(other.spellKey)) {\n            return false;\n        }\n        return true;\n    }\n\n    public static class SpellReference {\n        final Tools5eIndexType refererType;\n        final JsonNode refererNode;\n        final String refererName;\n\n        final String refererKey;\n        final String groupName;\n        final String classLevel;\n        final String spellLevel;\n        final String asLevel; // special case for known spells castable as cantrips\n        final boolean expanded;\n\n        public SpellReference(String key, boolean expanded) {\n            this(key, \"\", null, expanded, null);\n        }\n\n        public SpellReference(String key, String constraint, String asLevel, boolean expanded, String groupName) {\n            this.refererKey = key;\n            this.asLevel = asLevel;\n            this.expanded = expanded;\n            if (constraint.contains(\"_\")) {\n                this.classLevel = null;\n                this.spellLevel = null;\n            } else if (constraint.matches(\"^\\\\d+\")) {\n                this.classLevel = constraint;\n                this.spellLevel = null;\n            } else if (constraint.matches(\"^s\\\\d+\")) {\n                this.classLevel = null;\n                this.spellLevel = constraint.substring(1);\n            } else {\n                if (isPresent(constraint)) {\n                    Tui.instance().logf(Msg.UNKNOWN, \"%s: Unknown constraint [%s]\", key, constraint);\n                }\n                this.classLevel = null;\n                this.spellLevel = null;\n            }\n            this.refererType = Tools5eIndexType.getTypeFromKey(key);\n            this.refererNode = Tools5eIndex.instance().getOriginNoFallback(key);\n            this.refererName = linkifier().decoratedName(refererType, refererNode);\n            this.groupName = groupName;\n        }\n\n        boolean isSpecific() {\n            return classLevel != null\n                    || spellLevel != null\n                    || asLevel != null;\n        }\n\n        @Override\n        public int hashCode() {\n            final int prime = 31;\n            int result = 1;\n            result = prime * result + ((refererKey == null) ? 0 : refererKey.hashCode());\n            return result;\n        }\n\n        @Override\n        public boolean equals(Object obj) {\n            if (this == obj) {\n                return true;\n            }\n            if (obj == null) {\n                return false;\n            }\n            if (getClass() != obj.getClass()) {\n                return false;\n            }\n            SpellReference other = (SpellReference) obj;\n            if (refererKey == null) {\n                if (other.refererKey != null) {\n                    return false;\n                }\n            } else if (!refererKey.equals(other.refererKey)) {\n                return false;\n            }\n            return true;\n        }\n\n        @Override\n        public String toString() {\n            return refererKey + \";c:\" + classLevel + \";s:\" + spellLevel + \";\" + asLevel;\n        }\n\n        public String tagifyReference() {\n            String type = refererType.name().replace(\"type\", \"\");\n            return Stream.of(\"spell\", type, refererName, groupName)\n                    .filter(s -> isPresent(s))\n                    .map(Tui::slugify)\n                    .collect(Collectors.joining(\"/\"));\n        }\n\n        public String listFileName() {\n            return linkifier().getSpellList(refererName, Tools5eSources.findSources(refererNode));\n        }\n\n        public String linkifyReference() {\n            Tools5eIndex index = Tools5eIndex.instance();\n\n            List<String> linkSources = new ArrayList<>();\n            Tools5eSources sources = Tools5eSources.findSources(refererNode);\n            String linkText = linkifier().decoratedName(refererType, refererNode);\n            String resource = linkifier().getSpellList(linkText, sources);\n\n            if (refererType == Tools5eIndexType.subclass) {\n                String classKey = Tools5eIndexType.classtype.fromChildKey(refererKey);\n                JsonNode classNode = index.getOriginNoFallback(classKey);\n                String className = linkifier().decoratedName(Tools5eIndexType.classtype, classNode);\n                linkText = \"%s (%s%s)\".formatted(className, linkText, isPresent(groupName) ? \", \" + groupName : \"\");\n                linkSources.add(sourceString(refererType, sources.primarySource()));\n                linkSources.add(sourceString(Tools5eIndexType.classtype, SourceField.source.getTextOrEmpty(classNode)));\n            } else if (isPresent(groupName)) {\n                linkText = \"%s (%s)\".formatted(linkText, groupName);\n            }\n\n            linkSources.removeIf(String::isEmpty);\n            return \"[%s](%s%s/%s.md%s)\".formatted(linkText,\n                    Tools5eIndex.instance().compendiumVaultRoot(),\n                    linkifier().getRelativePath(Tools5eIndexType.spellIndex),\n                    resource,\n                    linkSources.isEmpty() ? \"\" : \" \\\"%s\\\"\".formatted(join(\";\", linkSources)));\n        }\n\n        private String sourceString(Tools5eIndexType type, String value) {\n            if (!isPresent(value) || type.defaultSourceString().equals(value)) {\n                return \"\";\n            }\n            return type.templateName() + \"=\" + value;\n        }\n\n        public String describe() {\n            List<String> append = new ArrayList<>();\n            if (isPresent(asLevel)) {\n                append.add(\"as \" + JsonSource.spellLevelToText(asLevel));\n            } else if (isPresent(spellLevel)) {\n                String display = JsonSource.spellLevelToText(spellLevel);\n                if (\"cantrip\".equals(display)) {\n                    display = \"cantrips\";\n                } else {\n                    display += \" spells\";\n                }\n                append.add(\"with access to \" + display);\n            }\n            if (isPresent(classLevel) && !\"1\".equals(classLevel)) {\n                append.add(\"at class level \" + classLevel);\n            }\n            return String.join(\", \", append);\n        }\n    }\n\n    private static Tools5eLinkifier linkifier() {\n        return Tools5eLinkifier.instance();\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/SpellIndex.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.TreeMap;\nimport java.util.stream.Collectors;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.config.TtrpgConfig;\nimport dev.ebullient.convert.io.Msg;\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.tools.JsonNodeReader;\nimport dev.ebullient.convert.tools.dnd5e.SpellEntry.SpellReference;\nimport dev.ebullient.convert.tools.dnd5e.Tools5eIndexType.IndexFields;\n\npublic class SpellIndex implements JsonSource {\n    private static final Set<String> skipReferences = Set.of(\n            \"subclass|college of lore|bard|phb|phb\");\n\n    final Map<String, SpellEntry> spellsByKey = new TreeMap<>();\n\n    private final Tools5eIndex index;\n\n    public SpellIndex(Tools5eIndex index) {\n        this.index = index;\n    }\n\n    public void clear() {\n        spellsByKey.clear();\n    }\n\n    public SpellEntry getSpellEntry(String key) {\n        key = index.getAliasOrDefault(key);\n        // getOrigin will log unresolved once.\n        return index.getOrigin(key) != null\n                ? spellsByKey.computeIfAbsent(key, k -> new SpellEntry(k, index.getOrigin(k)))\n                : null;\n    }\n\n    /**\n     * Add a spell to the index\n     * (while iterating elements in prepare)\n     *\n     * @param key\n     * @param spellNode\n     */\n    public SpellEntry addSpell(String key, JsonNode spellNode) {\n        key = index.getAliasOrDefault(key);\n        return spellsByKey.compute(key,\n                (k, v) -> v == null ? new SpellEntry(k, spellNode) : v);\n    }\n\n    /**\n     * Read spells/sources.json.\n     * Called at the end of {@link Tools5eIndex#prepare()}\n     * read additional spell sources to generate reference indexes of included\n     * spells\n     */\n    public void buildSpellIndex(Collection<JsonNode> allNodes) {\n        // Remove excluded spells ahead of any other iteration\n        spellsByKey.entrySet().removeIf(e -> index.isExcluded(e.getKey()));\n\n        // Read spells/sources.json; generate class index for filters\n        readSpellSources();\n        // now process additionalSpells nodes\n        processAdditionalSpells(allNodes);\n    }\n\n    /**\n     * Called from {@link #buildSpellIndex(Collection)} to read\n     * the generated/gendata-spell-source-lookup.json file and create\n     * indexes for filtering by class, subclass, feat, etc.\n     */\n    private void readSpellSources() {\n        // Iterate the contents of generated/gendata-spell-source-lookup.json\n        // This file is organized: source -> spellName -> type -> type-specific data\n        JsonNode spellClassMap = TtrpgConfig.readIndex(\"spell-source\");\n        // source -> spellName\n        for (var sourceToSpells : iterableFields(spellClassMap)) {\n            final String spellSource = sourceToSpells.getKey();\n            // spellName -> types\n            for (var nameToTypes : iterableFields(sourceToSpells.getValue())) {\n                final String spellName = nameToTypes.getKey();\n\n                String spellKey = Tools5eIndexType.spell.createKey(spellName, spellSource);\n                if (index.isExcluded(spellKey)) {\n                    continue;\n                }\n\n                final JsonNode spellNode = index().getOriginNoFallback(spellKey);\n                if (spellNode == null) {\n                    continue; // spell source not included\n                }\n                SpellEntry spellEntry = addSpell(spellKey, spellNode);\n\n                // type -> type-specific data\n                for (var typeEntry : iterableFields(nameToTypes.getValue())) {\n                    processSpellSourceType(spellEntry, typeEntry.getKey(), typeEntry.getValue());\n                }\n            }\n        }\n    }\n\n    /**\n     * Process a single reference type from the spell source lookup.\n     */\n    private void processSpellSourceType(SpellEntry spellEntry, String type, JsonNode typeData) {\n        switch (type) {\n            case \"class\" -> processClassRefs(spellEntry, typeData, false);\n            case \"classVariant\" -> processClassRefs(spellEntry, typeData, true);\n            case \"subclass\" -> processSubclassRefs(spellEntry, typeData);\n            default -> processSimpleRefs(spellEntry, typeData, type);\n        }\n    }\n\n    /**\n     * Process class or classVariant references.\n     * Structure for class: classSource -> { className: true, ... }\n     * Structure for classVariant: classSource -> { className: { definedInSources: [...] }, ... }\n     */\n    private void processClassRefs(SpellEntry spellEntry, JsonNode data, boolean expanded) {\n        for (var sourceEntry : iterableFields(data)) {\n            String classSource = sourceEntry.getKey();\n            for (var classEntry : iterableFields(sourceEntry.getValue())) {\n                String className = classEntry.getKey();\n\n                if (expanded) {\n                    // classVariant: check that at least one definedInSource is included\n                    JsonNode classValue = classEntry.getValue();\n                    JsonNode definedIn = classValue.isObject()\n                            ? classValue.get(\"definedInSources\")\n                            : null;\n                    if (definedIn != null && definedIn.isArray()) {\n                        boolean anyIncluded = false;\n                        for (var src : iterableElements(definedIn)) {\n                            if (index().sourceIncluded(src.asText())) {\n                                anyIncluded = true;\n                                break;\n                            }\n                        }\n                        if (!anyIncluded) {\n                            continue;\n                        }\n                    }\n                }\n\n                String refKey = Tools5eIndexType.classtype.createKey(className, classSource);\n                // Resolve reprints so we reference the newest version\n                String resolvedKey = index().getAliasOrDefault(refKey);\n                if (!refKey.equals(resolvedKey)) {\n                    continue; // Skip old reprinted version; the canonical version is handled separately\n                }\n                if (!index().isExcluded(resolvedKey) && index().getOriginNoFallback(resolvedKey) != null) {\n                    spellEntry.addSpellReference(resolvedKey, expanded);\n                }\n            }\n        }\n    }\n\n    /**\n     * Process subclass references.\n     * Structure: classSource -> { className: { scSource: { scShortName: { name: \"...\" } } } }\n     */\n    private void processSubclassRefs(SpellEntry spellEntry, JsonNode data) {\n        for (var classSourceEntry : iterableFields(data)) {\n            String classSource = classSourceEntry.getKey();\n            for (var classNameEntry : iterableFields(classSourceEntry.getValue())) {\n                String className = classNameEntry.getKey();\n                for (var scSourceEntry : iterableFields(classNameEntry.getValue())) {\n                    String scSource = scSourceEntry.getKey();\n                    for (var scEntry : iterableFields(scSourceEntry.getValue())) {\n                        // The value has { \"name\": \"Full Subclass Name\" }\n                        JsonNode scValue = scEntry.getValue();\n                        String scName = scValue.isObject() && scValue.has(\"name\")\n                                ? scValue.get(\"name\").asText()\n                                : scEntry.getKey();\n                        // subclass|subclassName|className|classSource|scSource\n                        String refKey = \"subclass|%s|%s|%s|%s\".formatted(\n                                scName, className, classSource, scSource).toLowerCase();\n                        // Resolve reprints so we reference the newest version\n                        String resolvedKey = index().getAliasOrDefault(refKey);\n                        if (!refKey.equals(resolvedKey)) {\n                            continue; // Skip old reprinted version; the canonical version is handled separately\n                        }\n                        if (!index().isExcluded(resolvedKey) && index().getOriginNoFallback(resolvedKey) != null) {\n                            spellEntry.addSpellReference(resolvedKey, false);\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Process simple reference types (feat, reward, background, race, optionalfeature).\n     * Structure: source -> { name: true|{...} }\n     */\n    private void processSimpleRefs(SpellEntry spellEntry, JsonNode data, String typeName) {\n        Tools5eIndexType refType = resolveRefType(typeName);\n        if (refType == null) {\n            tui().logf(Msg.UNKNOWN, \"Unknown spell source type: %s\", typeName);\n            return;\n        }\n        for (var sourceEntry : iterableFields(data)) {\n            String source = sourceEntry.getKey();\n            for (var nameEntry : iterableFields(sourceEntry.getValue())) {\n                String name = nameEntry.getKey();\n                String refKey = refType.createKey(name, source);\n                // Resolve aliases (e.g. race -> subrace) before lookup\n                String resolvedKey = index().getAliasOrDefault(refKey);\n                if (!index().isExcluded(resolvedKey) && index().getOriginNoFallback(resolvedKey) != null) {\n                    spellEntry.addSpellReference(resolvedKey, false);\n                }\n            }\n        }\n    }\n\n    private Tools5eIndexType resolveRefType(String typeName) {\n        return switch (typeName) {\n            case \"feat\" -> Tools5eIndexType.feat;\n            case \"optionalfeature\" -> Tools5eIndexType.optfeature;\n            case \"race\" -> Tools5eIndexType.race;\n            case \"background\" -> Tools5eIndexType.background;\n            case \"reward\" -> Tools5eIndexType.reward;\n            default -> null;\n        };\n    }\n\n    /**\n     * Called from {@link #buildSpellIndex(Collection)} to process\n     * `additionalSpells`\n     * nodes present in included content (can occur in a variety of types)\n     */\n    private void processAdditionalSpells(Collection<JsonNode> allNodes) {\n        // iterate over all included nodes\n        for (var node : allNodes) {\n            Tools5eSources sources = Tools5eSources.findSources(node);\n            final String nodeKey = sources.getKey();\n            if (skipReferences.contains(nodeKey) || index.isExcluded(nodeKey)) {\n                // beyond excluded classes, there are some hard-coded notes to skip...\n                continue;\n            }\n            // get the type of the node (spell, race, class, feat, ...)\n            final Tools5eIndexType type = sources.getType();\n            if (type == Tools5eIndexType.spell) {\n                // look for legacy specification of classes that can use a spell\n                // this method is used by homebrew\n                readClassesFromSpell(nodeKey, node);\n            } else {\n                // otherwise look for `additionalSpells` in the node\n                readAdditionalSpells(nodeKey, node);\n            }\n        }\n    }\n\n    /**\n     * Called from {@link #processAdditionalSpells(Collection)} to read\n     *\n     * @param spellKey the spell key\n     * @param spellNode the spell node\n     */\n    private void readClassesFromSpell(String spellKey, JsonNode spellNode) {\n        JsonNode classes = SpellIndexFields.classes.getFrom(spellNode);\n        if (classes == null || classes.isNull()) {\n            return;\n        }\n        // Find the created spellEntry (by key)\n        SpellEntry spellEntry = getSpellEntry(spellKey);\n        // Legacy / homebrew\n        for (var n : SpellIndexFields.fromClassList.iterateArrayFrom(classes)) {\n            tui().logf(Msg.SPELL, \"readClasses/fromClassList: %s :: %s\", spellKey, n);\n            readClassType(spellEntry, n, Tools5eIndexType.classtype);\n        }\n        for (var n : SpellIndexFields.fromClassListVariant.iterateArrayFrom(classes)) {\n            tui().logf(Msg.SPELL, \"readClasses/fromClassList: %s :: %s\", spellKey, n);\n            readClassType(spellEntry, n, Tools5eIndexType.classtype);\n        }\n        for (var n : SpellIndexFields.fromSubclass.iterateArrayFrom(classes)) {\n            tui().logf(Msg.SPELL, \"readClasses/fromSubclass: %s :: %s\", spellKey, n);\n            JsonNode classNode = SpellIndexFields.classNode.getFrom(n);\n            JsonNode subclassNode = SpellIndexFields.subclass.getFrom(n);\n            // Add class attributes to the subclass node so the key can\n            // be created as usual\n            IndexFields.className.setIn(subclassNode,\n                    SourceField.name.getTextOrNull(classNode));\n            IndexFields.classSource.setIn(subclassNode,\n                    SourceField.source.getTextOrNull(classNode));\n            readClassType(spellEntry, subclassNode, Tools5eIndexType.subclass);\n        }\n    }\n\n    /**\n     * Called from {@link #readClassesFromSpell(String, JsonNode)}\n     *\n     * @param spellEntry the spell entry\n     * @param reference the class or subclass node\n     * @param refType Tools5eIndexType.classtype or Tools5eIndexType.subclass\n     */\n    private void readClassType(SpellEntry spellEntry, JsonNode reference, Tools5eIndexType refType) {\n        // Resolve reprints so we reference the newest version\n        final String refKey = index().getAliasOrDefault(refType.createKey(reference));\n\n        // A book: TCE, for example, which made changes to Bard and Ranger..\n        String variantSource = SpellIndexFields.definedInSource.getTextOrNull(reference);\n\n        // skip (a) if reference is excluded, or\n        // (b) this is a variant and the variant source is excluded\n        if (index().isExcluded(refKey)\n                || (variantSource != null && !index().sourceIncluded(variantSource))) {\n            return;\n        }\n\n        spellEntry.addSpellReference(refKey, variantSource != null);\n    }\n\n    /**\n     * Called from {@link #processAdditionalSpells(Collection)} to read\n     *\n     * from: \"util-additionalspells.json\"\n     * schema: additionalSpellsArray\n     *\n     * @param refererKey the key of the referring node\n     * @param refererNode the referring node\n     */\n    private void readAdditionalSpells(String refererKey, JsonNode refererNode) {\n        final JsonNode additionalNode = SpellIndexFields.additionalSpells.getFrom(refererNode);\n        if (index.isExcluded(refererKey) || additionalNode == null || additionalNode.isNull()) {\n            // skip excluded nodes and nodes without an additionalSpells attribute\n            return;\n        }\n        // Collect all spells referenced by this element\n        for (var additionalSpells : iterableElements(additionalNode)) {\n            String groupName = SpellIndexFields.name.getTextOrNull(additionalSpells);\n            gatherSpells(refererKey, SpellIndexFields.innate.getFrom(additionalSpells), false, groupName);\n            gatherSpells(refererKey, SpellIndexFields.known.getFrom(additionalSpells), false, groupName);\n            gatherSpells(refererKey, SpellIndexFields.prepared.getFrom(additionalSpells), false, groupName);\n            gatherSpells(refererKey, SpellIndexFields.expanded.getFrom(additionalSpells), true, groupName);\n        }\n    }\n\n    /**\n     * Called from {@link #readAdditionalSpells(String, JsonNode)}\n     * from: \"util-additionalspells.json\"\n     * schema: _additionalSpellObject\n     *\n     * @param refererKey the key of the referring node\n     * @param spellList the _additionalSpellObject data from the referring node\n     * @param expanded true if this data expands/extends the class spell list\n     * @param groupName (optional) the name of the group of spells\n     */\n    private void gatherSpells(String refererKey, JsonNode spellList, boolean expanded, String groupName) {\n        // This is a rough ride. We need to handle a variety of formats\n        // Ultimately, we're just looking to find a list of touched/referenced spells\n        for (var properties : iterableFields(spellList)) {\n            toSpellList(refererKey, properties.getValue(), properties.getKey(), expanded, groupName);\n        }\n    }\n\n    /**\n     * Called from {@link #gatherSpells(String, JsonNode, boolean, String)}\n     *\n     * from: \"util-additionalspells.json\", schema noted below\n     *\n     * @param refererKey the key of the referring node\n     * @param spellList list of nodes (of various types) that reference spells\n     * @param constraint the constraint (the key: 1, s1, etc.)\n     * @param expanded true if this data expands/extends the class spell list\n     */\n    private void toSpellList(String refererKey, JsonNode spellList, String constraint, boolean expanded, String groupName) {\n        if (spellList == null || spellList.isNull()) {\n            return;\n        }\n        if (spellList.isObject()) {\n            // _additionalSpellLevelObject -> _additionalSpellRechargeObject\n            resolveRechargeSpells(refererKey, SpellIndexFields.rest.getFrom(spellList), constraint, expanded, groupName);\n            resolveRechargeSpells(refererKey, SpellIndexFields.daily.getFrom(spellList), constraint, expanded, groupName);\n            resolveRechargeSpells(refererKey, SpellIndexFields.resource.getFrom(spellList), constraint, expanded, groupName);\n\n            // recurse: these keys hold arrays of spells:\n            // _additionalSpellArrayOfStringOrFilterObject\n            toSpellList(refererKey, SpellIndexFields.ritual.getFrom(spellList), constraint, expanded, groupName);\n            toSpellList(refererKey, SpellIndexFields.will.getFrom(spellList), constraint, expanded, groupName);\n            toSpellList(refererKey, SpellIndexFields.others.getFrom(spellList), constraint, expanded, groupName);\n        } else if (spellList.isArray()) {\n            // _additionalSpellArrayOfStringOrFilterObject\n            for (var reference : iterableElements(spellList)) {\n                if (reference.isTextual()) {\n                    addFromText(refererKey, reference.asText(), constraint, expanded, groupName);\n                } else if (reference.isObject()) {\n                    // a filter defining referenced spells (where all would be included)\n                    resolveFilter(refererKey, SpellIndexFields.all.getFrom(reference), constraint, expanded, groupName);\n\n                    // a filter defining referenced spells (where some would be chosen)\n                    JsonNode choose = SpellIndexFields.choose.getFrom(reference);\n                    if (SpellIndexFields.from.existsIn(choose)) {\n                        // choose from a list of spell reference tags..\n                        for (var x : SpellIndexFields.from.iterateArrayFrom(choose)) {\n                            addFromText(refererKey, x.asText(), constraint, expanded, groupName);\n                        }\n                    } else {\n                        // handle the filter describing the spells to include\n                        resolveFilter(refererKey, choose, constraint, expanded, groupName);\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Called from {@link #toSpellList(String, JsonNode, String, boolean)}\n     *\n     * schema: _additionalSpellRechargeObject\n     *\n     * @param refererKey the key of the referring node\n     * @param rechargeNode the _additionalSpellRechargeObject data from the\n     *        referring node\n     * @param constraint the constraint (the key: 1, s1, etc.)\n     * @param expanded true if this data expands/extends the class spell list\n     */\n    public void resolveRechargeSpells(String refererKey, JsonNode rechargeNode, String constraint,\n            boolean expanded, String groupName) {\n        if (rechargeNode == null || rechargeNode.isNull()) {\n            return;\n        }\n        // we're ignoring the key here: 1, 2, 3, 4, ...; 1e, 2e, 3e...\n        // the value is _additionalSpellArrayOfStringOrFilterObject\n        for (var x : iterableFields(rechargeNode)) {\n            toSpellList(refererKey, x.getValue(), constraint, expanded, groupName);\n        }\n    }\n\n    /**\n     * A range of spells to be added, formatted similarly to the options in a\n     * {@literal {@filter ...}} tag. For example: {@code level=0|class=Wizard}\n     *\n     * @param refererKey the key of the referring node\n     * @param filter the filter node (should be text)\n     * @param constraint the constraint (the key: 1, s1, etc.)\n     * @param expanded true if this data expands/extends the class spell list\n     */\n    public void resolveFilter(String refererKey, JsonNode filter, String constraint, boolean expanded, String groupName) {\n        if (filter == null || filter.isNull()) {\n            return;\n        }\n        if (!filter.isTextual()) {\n            tui().logf(Msg.UNKNOWN, \"resolveFilter unknown value %s from %s\", filter, refererKey);\n            return;\n        }\n        tui().logf(Msg.SPELL, \"resolveFilter (%2s) %s :: %s\", constraint, refererKey, filter);\n        // level=1;2;3;4;5|class=Cleric;Druid;Wizard|school=D\n        String[] filterParts = filter.asText().split(\"\\\\|\");\n        FilterConditions filterConditions = new FilterConditions();\n\n        for (String f : filterParts) {\n            String[] parts = f.split(\"=\");\n            if (parts.length == 2) {\n                switch (parts[0].toLowerCase()) {\n                    case \"class\" -> {\n                        filterConditions.setClasses(parts[1].split(\";\"));\n                    }\n                    case \"level\" -> {\n                        filterConditions.setLevels(parts[1].split(\";\"));\n                    }\n                    case \"school\" -> {\n                        filterConditions.setSchools(parts[1].split(\";\"));\n                    }\n                    case \"source\" -> {\n                        filterConditions.setSources(parts[1].split(\";\"));\n                    }\n                    case \"spell attack\" -> {\n                        filterConditions.setSpellAttack(parts[1].split(\";\"));\n                    }\n                    case \"components & miscellaneous\" -> {\n                        filterConditions.setComponentsMisc(parts[1].split(\";\"));\n                    }\n                    default ->\n                        tui().logf(Msg.UNKNOWN, \"resolveFilter unknown part: %s\", parts[0]);\n                }\n            }\n        }\n        for (SpellEntry spell : spellsByKey.values()) {\n            if (filterConditions.matchAll(spell)) {\n                spell.addReference(new SpellReference(refererKey, constraint, null, expanded, groupName));\n            }\n        }\n    }\n\n    /**\n     * Called from {@link #toSpellList(String, JsonNode, String, boolean)}\n     *\n     * @param refererKey the key of the referring node\n     * @param tag the spell reference tag\n     * @param constraint the constraint (the key: 1, s1, etc.)\n     * @param expanded true if this data expands/extends the class spell list\n     */\n    private void addFromText(String refererKey, String tag, String constraint, boolean expanded, String groupName) {\n        int pos = tag.indexOf(\"#\");\n        String asLevel = pos > 0 ? tag.substring(pos + 1) : null;\n        tag = pos > 0 ? tag.substring(0, pos) : tag;\n\n        String spellKey = Tools5eIndexType.spell.fromTagReference(tag);\n        if (index.isExcluded(spellKey)) {\n            return;\n        }\n        var spellEntry = getSpellEntry(spellKey);\n        if (spellEntry != null) {\n            spellEntry.addReference(refererKey, constraint, asLevel, expanded, groupName);\n        }\n    }\n\n    static class FilterConditions {\n        Set<String> classes = Set.of();\n        Set<String> levels = Set.of();\n        Set<String> schools = Set.of();\n        Set<String> sources = Set.of();\n        Set<String> spellAttack = Set.of();\n        Set<String> componentsMisc = Set.of();\n\n        /**\n         * @param class the class to set\n         */\n        public void setClasses(String[] classList) {\n            this.classes = Arrays.stream(classList)\n                    .map(x -> x.toLowerCase())\n                    .collect(Collectors.toSet());\n        }\n\n        /**\n         * @param level the level to set\n         */\n        public void setLevels(String[] level) {\n            this.levels = Set.of(level);\n        }\n\n        /**\n         * @param school the school to set\n         */\n        public void setSchools(String[] school) {\n            this.schools = Set.of(school);\n        }\n\n        /**\n         * @param source the source to set\n         */\n        public void setSources(String[] source) {\n            this.sources = Set.of(source);\n        }\n\n        /**\n         * @param source the source to set\n         */\n        public void setSpellAttack(String[] source) {\n            this.spellAttack = Arrays.stream(source)\n                    .map(x -> x.toUpperCase())\n                    .collect(Collectors.toSet());\n        }\n\n        public void setComponentsMisc(String[] components) {\n            this.componentsMisc = Arrays.stream(components)\n                    .map(x -> x.toLowerCase())\n                    .collect(Collectors.toSet());\n        }\n\n        boolean matchAll(SpellEntry spell) {\n            Tools5eSources spellSources = Tools5eSources.findSources(spell.spellNode);\n            return testClasses(spell)\n                    && testLevels(spell)\n                    && testSchools(spell)\n                    && testSources(spell, spellSources)\n                    && testSpellAttack(spell)\n                    && testSpellComponents(spell);\n        }\n\n        private boolean testClasses(SpellEntry spell) {\n            return classes.isEmpty() || classes.stream().anyMatch(x -> spell.inClassList(x));\n        }\n\n        private boolean testLevels(SpellEntry spell) {\n            return levels.isEmpty() || levels.contains(spell.level);\n        }\n\n        private boolean testSchools(SpellEntry spell) {\n            return schools.isEmpty() || schools.contains(spell.school.code());\n        }\n\n        private boolean testSources(SpellEntry spell, Tools5eSources spellSources) {\n            return sources.isEmpty() || spellSources.includedBy(sources);\n        }\n\n        private boolean testSpellAttack(SpellEntry spell) {\n            return spellAttack.isEmpty()\n                    || spell.spellAttack.stream().anyMatch(x -> spellAttack.contains(x.toUpperCase()));\n        }\n\n        private boolean testSpellComponents(SpellEntry spell) {\n            if (componentsMisc.contains(\"ritual\")) {\n                return spell.ritual;\n            } else if (!componentsMisc.isEmpty()) {\n                Tui.instance().logf(Msg.UNKNOWN, \"Unknown components & miscellaneous value: %s\", componentsMisc);\n            }\n            return true;\n        }\n    }\n\n    enum SpellIndexFields implements JsonNodeReader {\n        ability,\n        additionalSpells,\n        all,\n        choose,\n        classes,\n        classNode(\"class\"),\n        components,\n        daily,\n        definedInSource,\n        expanded,\n        from,\n        fromClassList,\n        fromClassListVariant,\n        fromSubclass,\n        innate,\n        known,\n        level,\n        meta,\n        name,\n        others(\"_\"),\n        prepared,\n        resource,\n        resourceName,\n        rest,\n        ritual,\n        school,\n        spellAttack,\n        subclass,\n        text,\n        will,\n        ;\n\n        private final String nodeName;\n\n        SpellIndexFields(String nodeName) {\n            this.nodeName = nodeName;\n        }\n\n        SpellIndexFields() {\n            this.nodeName = name();\n        }\n\n        @Override\n        public String nodeName() {\n            return nodeName;\n        }\n    }\n\n    @Override\n    public Tools5eIndex index() {\n        return index;\n    }\n\n    @Override\n    public Tools5eSources getSources() {\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/SpellSchool.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\npublic interface SpellSchool {\n\n    String name();\n\n    String code();\n\n    record CustomSpellSchool(String code, String name) implements SpellSchool {\n        @Override\n        public String code() {\n            return code;\n        }\n\n        @Override\n        public String name() {\n            return name;\n        }\n    }\n\n    enum SchoolEnum implements SpellSchool {\n        Abjuration(\"A\"),\n        Conjuration(\"C\"),\n        Divination(\"D\"),\n        Enchantment(\"E\"),\n        Evocation(\"V\"),\n        Illusion(\"I\"),\n        Necromancy(\"N\"),\n        Transmutation(\"T\"),\n        Psychic(\"P\"),\n        None(\"_\");\n\n        private final String code;\n\n        SchoolEnum(String abbreviation) {\n            this.code = abbreviation;\n        }\n\n        public String code() {\n            return code;\n        }\n    }\n\n    static SpellSchool fromEncodedValue(String v) {\n        if (v == null || v.isBlank()) {\n            return null;\n        }\n        return switch (v) {\n            case \"A\" -> SchoolEnum.Abjuration;\n            case \"C\" -> SchoolEnum.Conjuration;\n            case \"D\" -> SchoolEnum.Divination;\n            case \"E\", \"EN\" -> SchoolEnum.Enchantment;\n            case \"V\", \"EV\" -> SchoolEnum.Evocation;\n            case \"I\" -> SchoolEnum.Illusion;\n            case \"N\" -> SchoolEnum.Necromancy;\n            case \"T\" -> SchoolEnum.Transmutation;\n            default -> {\n                String tolower = v.toLowerCase();\n                for (SpellSchool s : SchoolEnum.values()) {\n                    if (s.name().toLowerCase().equals(tolower)) {\n                        yield s;\n                    }\n                }\n                yield null;\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndex.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static dev.ebullient.convert.StringUtil.isPresent;\n\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.TreeMap;\nimport java.util.TreeSet;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.function.BiConsumer;\nimport java.util.stream.Collectors;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.BooleanNode;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\n\nimport dev.ebullient.convert.config.CompendiumConfig;\nimport dev.ebullient.convert.config.ReprintBehavior;\nimport dev.ebullient.convert.config.TtrpgConfig;\nimport dev.ebullient.convert.io.MarkdownWriter;\nimport dev.ebullient.convert.io.Msg;\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.qute.SourceAndPage;\nimport dev.ebullient.convert.tools.MarkdownConverter;\nimport dev.ebullient.convert.tools.ToolsIndex;\nimport dev.ebullient.convert.tools.dnd5e.HomebrewIndex.HomebrewFields;\nimport dev.ebullient.convert.tools.dnd5e.HomebrewIndex.HomebrewMetaTypes;\nimport dev.ebullient.convert.tools.dnd5e.Json2QuteClass.SubclassFeatureKeyData;\nimport dev.ebullient.convert.tools.dnd5e.Json2QuteItem.ItemField;\nimport dev.ebullient.convert.tools.dnd5e.Json2QuteRace.RaceFields;\nimport dev.ebullient.convert.tools.dnd5e.OptionalFeatureIndex.OptionalFeatureType;\nimport dev.ebullient.convert.tools.dnd5e.SkillOrAbility.CustomSkillOrAbility;\nimport dev.ebullient.convert.tools.dnd5e.SpellSchool.CustomSpellSchool;\n\npublic class Tools5eIndex implements JsonSource, ToolsIndex {\n    private static Tools5eIndex instance;\n\n    public static Tools5eIndex instance() {\n        return instance;\n    }\n\n    public static boolean isSrdBasicOnly() {\n        if (instance == null || !instance.prepared.get()) {\n            throw new IllegalStateException(\"Programmer error: Called isSrdBasicFreeOnly while indexing\");\n        }\n\n        if (instance.isSrdBasicFreeOnly == null) {\n            CompendiumConfig config = TtrpgConfig.getConfig();\n            instance.isSrdBasicFreeOnly = config.noSources()\n                    || config.onlySources(List.of(\"srd\", \"basicrules\", \"srd52\", \"basicrules2024\"));\n        }\n        return instance.isSrdBasicFreeOnly;\n    }\n\n    private Boolean isSrdBasicFreeOnly;\n    protected final AtomicBoolean prepared = new AtomicBoolean(false);\n\n    // Initialization\n    private final Map<String, JsonNode> nodeIndex = new TreeMap<>(); // --index\n\n    private final Map<String, JsonNode> subraces = new HashMap<>(); // --index\n    private final Map<SourceAndPage, List<JsonNode>> tableIndex = new HashMap<>();\n\n    private final Map<String, String> aliases = new TreeMap<>(); // --index\n    private final Map<String, String> reprints = new TreeMap<>(); // --index\n    private final Map<String, String> subraceMap = new TreeMap<>(); // --index\n    private final Map<String, String> nameToLink = new HashMap<>();\n\n    // Class feature, Subclass, and Subclass Feature nonsense\n    private final Map<String, Set<String>> classFeatures = new TreeMap<>(); // --index\n    private final Map<String, Set<String>> subclassMap = new TreeMap<>(); // --index\n\n    // Legendary group key -> monster keys that reference it\n    private final Map<String, Set<String>> legendaryGroupMonsters = new HashMap<>();\n\n    // Table keys that are actually linked from included (non-table) content\n    private final Set<String> referencedTableKeys = new HashSet<>();\n\n    private final Set<String> unresolvableKeys = new TreeSet<>();\n    private final Map<String, SkillOrAbility> resolvedSkills = new HashMap<>();\n\n    private final Set<String> srdKeys = new HashSet<>();\n\n    final OptionalFeatureIndex optFeatureIndex = new OptionalFeatureIndex(this);\n    final HomebrewIndex homebrewIndex = new HomebrewIndex(this);\n    final SpellIndex spellIndex = new SpellIndex(this);\n\n    final CompendiumConfig config;\n    final Tools5eJsonSourceCopier copier = new Tools5eJsonSourceCopier(this);\n\n    private Map<String, JsonNode> filteredIndex = null;\n\n    // transitory index state\n    volatile HomebrewMetaTypes homebrew = null;\n\n    public Tools5eIndex(CompendiumConfig config) {\n        this.config = config;\n        instance = this;\n    }\n\n    public Tools5eIndex importTree(String filename, JsonNode node) {\n        if (!node.isObject() || homebrewIndex.addHomebrewSourcesIfPresent(filename, node)) {\n            // defer reading contents of homebrew until after we've indexed the rest\n            // see prepare()  / importHomebrewTree()\n            return this;\n        }\n\n        // user configuration\n        config.readConfigurationIfPresent(node);\n\n        // Index content types\n        indexTypes(filename, node);\n\n        return this;\n    }\n\n    private void importHomebrewTree(HomebrewMetaTypes homebrew) {\n        this.homebrew = homebrew;\n        try {\n            // Index content types\n            indexTypes(homebrew.filename, homebrew.homebrewNode);\n            Tools5eIndexType.adventureData.withArrayFrom(homebrew.homebrewNode, this::addToIndex); // homebrew\n            Tools5eIndexType.bookData.withArrayFrom(homebrew.homebrewNode, this::addToIndex); // homebrew\n        } finally {\n            this.homebrew = null;\n        }\n    }\n\n    private void indexTypes(String filename, JsonNode node) {\n\n        // Reference/Internal Types\n\n        Tools5eIndexType.backgroundFluff.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.classFluff.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.conditionFluff.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.facilityFluff.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.featFluff.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.hazardFluff.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.itemFluff.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.objectFluff.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.optionalfeatureFluff.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.raceFluff.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.rewardFluff.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.spellFluff.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.subclassFluff.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.trapFluff.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.vehicleFluff.withArrayFrom(node, this::addToIndex);\n\n        Tools5eIndexType.monsterFluff.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.monsterFluff.withArrayFrom(node, \"creatureFluff\", this::addToIndex);\n\n        Tools5eIndexType.language.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.citation.withArrayFrom(node, this::addToIndex);\n\n        Tools5eIndexType.itemEntry.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.itemGroup.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.itemTypeAdditionalEntries.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.card.withArrayFrom(node, this::addToIndex);\n\n        Tools5eIndexType.magicvariant.withArrayFrom(node, this::addMagicVariantToIndex);\n        Tools5eIndexType.subrace.withArrayFrom(node, this::addToSubraceIndex);\n\n        Tools5eIndexType.monsterTemplate.withArrayFrom(node, this::addToIndex);\n\n        // Class-scoped resources (if the class is left out, the resource is not included)\n\n        Tools5eIndexType.subclass.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.classfeature.withArrayFrom(node, \"classFeature\", this::addToIndex);\n        Tools5eIndexType.subclassFeature.withArrayFrom(node, \"subclassFeature\", this::addToIndex);\n\n        // Output Types\n\n        Tools5eIndexType.action.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.condition.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.disease.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.itemMastery.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.itemProperty.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.itemType.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.sense.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.skill.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.status.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.variantrule.withArrayFrom(node, this::addToIndex);\n\n        Tools5eIndexType.psionic.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.legendaryGroup.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.optfeature.withArrayFrom(node, \"optionalfeature\", this::addToIndex);\n\n        // tables\n\n        Tools5eIndexType.table.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.tableGroup.withArrayFrom(node, this::addToIndex);\n\n        // templated types\n\n        Tools5eIndexType.background.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.classtype.withArrayFrom(node, \"class\", this::addToIndex);\n        Tools5eIndexType.deck.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.deity.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.facility.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.feat.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.hazard.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.item.withArrayFrom(node, \"baseitem\", this::addBaseItemToIndex);\n        Tools5eIndexType.item.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.monster.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.object.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.race.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.reward.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.spell.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.trap.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.vehicle.withArrayFrom(node, this::addToIndex);\n\n        Tools5eIndexType.adventure.withArrayFrom(node, this::addToIndex);\n        Tools5eIndexType.book.withArrayFrom(node, this::addToIndex);\n\n        // 5e tools book/adventure data\n        if (node.has(\"data\") && !filename.isEmpty()) {\n            int slash = filename.indexOf('/');\n            int dot = filename.indexOf('.');\n            String basename = filename.substring(slash < 0 ? 0 : slash + 1, dot < 0 ? filename.length() : dot);\n            String id = basename.replace(\"book-\", \"\").replace(\"adventure-\", \"\");\n            TtrpgConfig.includeAdditionalSource(id);\n            ((ObjectNode) node).put(\"id\", id);\n            addToIndex(basename.startsWith(\"book\") ? Tools5eIndexType.bookData : Tools5eIndexType.adventureData,\n                    node);\n        }\n    }\n\n    void addToSubraceIndex(Tools5eIndexType type, JsonNode node) {\n        subraces.put(type.createKey(node), node);\n    }\n\n    void addMagicVariantToIndex(Tools5eIndexType type, JsonNode node) {\n        MagicVariant.populateGenericVariant(node);\n\n        addToIndex(type, node);\n    }\n\n    void addBaseItemToIndex(Tools5eIndexType type, JsonNode node) {\n        TtrpgValue.indexBaseItem.setIn(node, BooleanNode.TRUE);\n        addToIndex(type, node);\n    }\n\n    void addToIndex(Tools5eIndexType type, JsonNode node) {\n        String key = type.createKey(node);\n        if (nodeIndex.containsKey(key)) {\n            return;\n        }\n        nodeIndex.put(key, node);\n        TtrpgValue.indexInputType.setIn(node, type.name());\n        TtrpgValue.indexKey.setIn(node, key);\n\n        // Homebrew files are ingested in a lump:\n        // if homebrew is set, then we're reading a homebrew file\n        TtrpgValue.isHomebrew.setIn(node, homebrew != null);\n        if (homebrew != null) {\n            homebrew.addCrossReference(type, key, node);\n        }\n\n        switch (type) {\n            case optfeature -> {\n                // add while we're ingesting (homebrew or not)\n                // will create/register an optionalFeatureType node\n                optFeatureIndex.addOptionalFeature(key, node, homebrew);\n            }\n            case subclass -> {\n                // add alias with subclass shortname\n                String lookupKey = Tools5eIndexType.getSubclassKey(\n                        Tools5eFields.className.getTextOrEmpty(node),\n                        Tools5eFields.classSource.getTextOrEmpty(node),\n                        Tools5eFields.shortName.getTextOrEmpty(node),\n                        SourceField.source.getTextOrEmpty(node));\n                addAlias(lookupKey, key);\n                classFeatures.put(key, new HashSet<>());\n            }\n            case table, tableGroup -> {\n                SourceAndPage sp = new SourceAndPage(node);\n                tableIndex.computeIfAbsent(sp, k -> new ArrayList<>()).add(node);\n                if (type == Tools5eIndexType.tableGroup) {\n                    addAlias(key.replace(\"tablegroup\", \"table\"), key);\n                }\n            }\n            case language -> {\n                if (HomebrewFields.fonts.existsIn(node)) {\n                    Tools5eSources.addFonts(node, HomebrewFields.fonts);\n                }\n            }\n            case adventure, book -> {\n                String id = SourceField.id.getTextOrEmpty(node);\n                String source = SourceField.source.getTextOrEmpty(node);\n                if (!id.equals(source) && type == Tools5eIndexType.book) {\n                    // adventures can be subdivided from books. Don't map source/id for those\n                    TtrpgConfig.sourceToIdMapping(source, id);\n                }\n                String parentSource = Tools5eFields.parentSource.getTextOrNull(node);\n                if (parentSource != null && TtrpgConfig.getConfig().sourceIncluded(source)) {\n                    // include the parent source if you include an adventure (related rules)\n                    tui().debugf(Msg.SOURCE, \"including %s due to %s\", parentSource, source);\n                    TtrpgConfig.includeAdditionalSource(parentSource);\n                }\n            }\n            case itemGroup -> {\n                addAlias(key.replace(\"itemgroup|\", \"item|\"), key);\n            }\n            default -> {\n            }\n        }\n\n        addSrdEntry(key, node);\n    }\n\n    public void prepare() {\n        if (filteredIndex != null) {\n            return;\n        }\n\n        // we're finished with discovery of official/homebrew sources\n        prepared.set(true);\n\n        tui().verbosef(\"Adding default aliases\");\n\n        // Add missing/frequently-used aliases\n        TtrpgConfig.addDefaultAliases(aliases);\n        TtrpgConfig.addReferenceEntries((n) -> addToIndex(Tools5eIndexType.reference, n));\n\n        // Properly import homebrew sources\n        tui().infof(Msg.BREW, \"Importing homebrew sources\");\n        homebrewIndex.importBrew(this::importHomebrewTree);\n        tui().verbosef(Msg.BREW, \"Finished with homebrew sources\");\n\n        tui().debugf(\"Preparing index using configuration:\\n%s\", Tui.jsonStringify(config));\n\n        // Add subraces to index\n        defineSubraces();\n\n        tui().verbosef(\"Resolving copies and linking sources\");\n\n        // Find remaining/included base items\n        List<JsonNode> baseItems = nodeIndex.values().stream()\n                .filter(n -> TtrpgValue.indexBaseItem.booleanOrDefault(n, false))\n                .filter(n -> !ItemField.packContents.existsIn(n))\n                .toList();\n\n        List<String> keys = new ArrayList<>(nodeIndex.keySet());\n        List<Tuple> deities = new ArrayList<>();\n\n        // For each node: handle copies, link sources\n        for (String key : keys) {\n            JsonNode jsonSource = nodeIndex.get(key);\n\n            // check for / manage copies first.\n            Tools5eIndexType type = Tools5eIndexType.getTypeFromKey(key);\n            jsonSource = copier.handleCopy(type, jsonSource);\n            nodeIndex.put(key, jsonSource); // update value with resolved/copied node\n\n            // Pre-creation of sources..\n            switch (type) {\n                case adventureData, bookData -> linkSources(type, jsonSource);\n                default -> {\n                }\n            }\n\n            Tools5eSources.constructSources(key, jsonSource);\n\n            if (type == Tools5eIndexType.deity) {\n                deities.add(new Tuple(key, jsonSource));\n                continue; // deal with these later.\n            }\n\n            // Reprints follow specialized variants, so we need to find the variants\n            // now (and will filter them out based on rules later...)\n            if (type.hasVariants()) {\n                List<JsonNode> variants = findVariants(key, jsonSource, baseItems);\n                for (JsonNode variant : variants) {\n                    String variantKey = TtrpgValue.indexKey.getTextOrThrow(variant);\n                    Tools5eSources.constructSources(variantKey, variant);\n                    JsonNode old = nodeIndex.put(variantKey, variant);\n                    if (old != null && !old.equals(variant)) {\n                        tui().errorf(\"Duplicate key: %s%nold: %s%nnew: %s\", variantKey, old, variant);\n                    }\n                    // Record legendary group references from monsters\n                    if (type == Tools5eIndexType.monster) {\n                        JsonNode lgRef = Json2QuteMonster.MonsterFields.legendaryGroup.getFrom(variant);\n                        if (lgRef != null) {\n                            String lgKey = Tools5eIndexType.legendaryGroup.createKey(lgRef);\n                            legendaryGroupMonsters.computeIfAbsent(lgKey, k -> new HashSet<>()).add(variantKey);\n                        }\n                    }\n                }\n            }\n\n            // Post-creation of sources..\n            switch (type) {\n                case classtype, subclass -> optFeatureIndex.amendSources(key, jsonSource);\n                case optfeature -> optFeatureIndex.amendSources(key, jsonSource);\n                default -> {\n                }\n            }\n        } // end for each entry\n\n        tui().progressf(\"Applying source filters\");\n        filteredIndex = new HashMap<>(nodeIndex.size());\n\n        BiConsumer<Msg, String> logThis = (msgType, msg) -> {\n            if (msgType == Msg.TARGET) {\n                tui().debugf(msgType, msg);\n            } else {\n                tui().logf(msgType, msg);\n            }\n        };\n\n        // Let's create a list of interesting keys\n        List<String> interestingKeys = new ArrayList<>(nodeIndex.size());\n        for (var e : nodeIndex.entrySet()) {\n            String key = e.getKey();\n            JsonNode jsonSource = e.getValue();\n            Tools5eIndexType type = Tools5eIndexType.getTypeFromKey(key);\n            if (false\n                    // Fluff types can continue to live only in the origin/nodeIndex\n                    || type.isFluffType()\n                    // Checking for reprints / aliasing knock-ons.\n                    || isReprinted(key, jsonSource)\n                    // While Deities are interesting, their handling is unique and done later\n                    || type == Tools5eIndexType.deity\n                    // Legendary groups are filtered by reference in a post-loop block\n                    || type == Tools5eIndexType.legendaryGroup\n                    // Subclasses are also handled backwards (filled in by subclass features)\n                    || type == Tools5eIndexType.subclass) {\n                // Theses are uninteresting.\n            } else {\n                interestingKeys.add(key);\n            }\n        }\n\n        // Apply include/exclude rules & source filters\n        // to add included elements to the filter index\n        for (String key : interestingKeys) {\n            JsonNode jsonSource = getOriginNoFallback(key);\n            if (jsonSource == null) {\n                continue;\n            }\n            Tools5eSources sources = Tools5eSources.findSources(key);\n            Tools5eIndexType type = Tools5eIndexType.getTypeFromKey(key);\n            Msg msgType = sources.filterRuleApplied() ? Msg.TARGET : Msg.FILTER;\n            if (type.isDependentType()) {\n                // dependent types: don't keep if parent is excluded/missing\n                if (processDependentType(key)) {\n                    logThis.accept(msgType, \" ----  \" + key);\n                    filteredIndex.put(key, jsonSource);\n                } else {\n                    logThis.accept(msgType, \"(drop) \" + key);\n                }\n            } else if (sources.includedByConfig()) {\n                // key is included (by a specific rule, or because the source is included)\n                filteredIndex.put(key, jsonSource);\n                logThis.accept(msgType, \" ----  \" + key);\n\n                if (type == Tools5eIndexType.spell) {\n                    // Create a spell entry for included spell\n                    spellIndex.addSpell(key, jsonSource);\n                }\n            } else {\n                // source is not included, item is dropped\n                logThis.accept(msgType, \"(drop) \" + key);\n            }\n        }\n\n        // Legendary groups: include only if referenced by an included monster\n        tui().verbosef(\"Filtering legendary groups\");\n        for (var e : nodeIndex.entrySet()) {\n            String key = e.getKey();\n            if (Tools5eIndexType.getTypeFromKey(key) != Tools5eIndexType.legendaryGroup) {\n                continue;\n            }\n            // Skip if aliased/reprinted to a different key\n            if (!key.equals(getAliasOrDefault(key))) {\n                continue;\n            }\n            Tools5eSources sources = Tools5eSources.findSources(key);\n            if (sources == null || !sources.includedByConfig()) {\n                continue;\n            }\n            Msg msgType = sources.filterRuleApplied() ? Msg.TARGET : Msg.FILTER;\n\n            // Explicit filter rule overrides reference check\n            if (sources.filterRuleApplied()) {\n                filteredIndex.put(key, e.getValue());\n                logThis.accept(msgType, \" ----  \" + key);\n                continue;\n            }\n            // Include only if referenced by an included monster.\n            // Don't resolve aliases: a reprinted monster won't be in filteredIndex,\n            // so it correctly won't count as a reference for the old legendary group.\n            Set<String> monsters = legendaryGroupMonsters.get(key);\n            boolean referenced = monsters != null && monsters.stream()\n                    .anyMatch(filteredIndex::containsKey);\n            if (referenced) {\n                filteredIndex.put(key, e.getValue());\n                logThis.accept(msgType, \" ----  \" + key);\n            } else {\n                logThis.accept(msgType, \"(drop | unreferenced) \" + key);\n            }\n        }\n\n        // classFeatures contains both features and subclass features\n        for (var entry : classFeatures.entrySet()) {\n            String scKey = entry.getKey();\n            if (scKey.startsWith(\"subclass\")) {\n                if (entry.getValue().isEmpty()) {\n                    // no features associated with this subclass\n                    logThis.accept(Msg.CLASSES, \"(drop | no subclass features) \" + scKey);\n                } else {\n                    logThis.accept(Msg.CLASSES, \" ----  \" + scKey);\n                    filteredIndex.put(scKey, nodeIndex.get(scKey));\n                }\n            }\n        }\n\n        // Remove unused optional features from the optional feature index\n        optFeatureIndex.removeUnusedOptionalFeatures(\n                (k) -> filteredIndex.containsKey(k),\n                (k) -> logThis.accept(Msg.FEATURETYPE, \" ----  \" + k),\n                (k) -> {\n                    Tools5eSources sources = Tools5eSources.findSources(k);\n                    if (sources.filterRuleApplied()) {\n                        return; // keep because a rule says so (we already logged these)\n                    }\n                    logThis.accept(Msg.FEATURETYPE, \"(drop) \" + k);\n                    filteredIndex.remove(k);\n                });\n\n        // Deities have their own glorious reprint mess, which we only need to deal with\n        // when we aren't hoarding all the things.\n        tui().verbosef(\"Dealing with deities\");\n        // Find deities that have not been superceded by a reprint\n        Json2QuteDeity.findDeities(deities).forEach(k -> {\n            filteredIndex.put(k, nodeIndex.get(k));\n        });\n\n        // And finally, create an index of classes/subclasses/feats for spells\n        // based on included sources & avaiable spells.\n        spellIndex.buildSpellIndex(filteredIndex.values());\n    }\n\n    private void defineSubraces() {\n        tui().verbosef(\"Adding subraces\");\n\n        Map<String, Set<JsonNode>> raceToSubraces = new HashMap<>();\n\n        for (JsonNode subrace : subraces.values()) {\n            subrace = copier.handleCopy(Tools5eIndexType.subrace, subrace);\n            String raceName = RaceFields.raceName.getTextOrThrow(subrace);\n            String raceSource = RaceFields.raceSource.getTextOrThrow(subrace);\n            String raceKey = Tools5eIndexType.race.createKey(raceName, raceSource);\n\n            raceToSubraces.computeIfAbsent(raceKey, k -> new HashSet<>()).add(subrace);\n        }\n\n        for (var entry : raceToSubraces.entrySet()) {\n            String raceKey = entry.getKey();\n            JsonNode jsonSource = nodeIndex.get(raceKey);\n\n            Set<JsonNode> inputSubraces = entry.getValue();\n            List<JsonNode> subraces = new ArrayList<>();\n\n            Json2QuteRace.prepareBaseRace(this, jsonSource, inputSubraces);\n\n            if (inputSubraces.size() > 1) {\n                tui().logf(Msg.RACES, \"%s subraces found for %s\", inputSubraces.size(), raceKey);\n            }\n\n            for (JsonNode sr : inputSubraces) {\n                sr = copier.mergeSubrace(sr, jsonSource);\n                String srKey = Tools5eIndexType.subrace.createKey(sr);\n                TtrpgValue.indexInputType.setIn(sr, Tools5eIndexType.subrace.name());\n                TtrpgValue.indexKey.setIn(sr, srKey);\n\n                nodeIndex.put(srKey, sr);\n                addSrdEntry(srKey, sr);\n                subraces.add(sr);\n\n                // Add expected alias:  {@race Aasimar (Fallen)|VGM}\n                String[] parts = srKey.split(\"\\\\|\");\n                String source = SourceField.source.getTextOrThrow(sr);\n                final String lookupKey;\n                if (parts[1].contains(\"(\")) { // \"subrace|dwarf (duergar)|dwarf|phb|mtf\"\n                    lookupKey = String.format(\"race|%s|%s\", parts[1], source).toLowerCase();\n                } else { // \"subrace|half-elf|half-elf|phb\"\n                    if (parts[1].equals(parts[2])) {\n                        lookupKey = String.format(\"race|%s|%s\", parts[1], source).toLowerCase();\n                    } else {\n                        lookupKey = String.format(\"race|%s (%s)|%s\", parts[2], parts[1], source).toLowerCase();\n                    }\n                }\n                // lookups from race to subrace are necessary, but can conflict with reprints/aliases\n                // keep them separate (still used in getAliasOrDefault)\n                subraceMap.put(lookupKey, srKey);\n                tui().logf(Msg.RACES, \"\\t%s :: %s\", lookupKey, srKey);\n            }\n\n            Json2QuteRace.updateBaseRace(this, jsonSource, inputSubraces, subraces);\n        }\n        subraces.clear();\n    }\n\n    List<JsonNode> findVariants(String key, JsonNode jsonSource, List<JsonNode> baseItems) {\n        Tools5eIndexType type = Tools5eIndexType.getTypeFromKey(key);\n        if (type == Tools5eIndexType.magicvariant) {\n            return MagicVariant.findSpecificVariants(this, type, key, jsonSource, copier, baseItems);\n        } else if (type == Tools5eIndexType.monster) {\n            // monster variants will always replace the original\n            nodeIndex.remove(key);\n            return Json2QuteMonster.findMonsterVariants(this, type, key, jsonSource);\n        }\n        return List.of(jsonSource);\n    }\n\n    /**\n     * Behavior of looking for reprints is changed by reprint behavior defined in configuration.\n     * The default is \"newest\", which will always collapse reprints into the newest source.\n     *\n     * @param finalKey\n     * @param jsonSource\n     * @return\n     */\n    private boolean isReprinted(String finalKey, JsonNode jsonSource) {\n        // This method assumes that excluded sources are already filtered out\n        if (config.reprintBehavior() == ReprintBehavior.all) {\n            return false; // ignore reprints and include everything ;)\n        }\n        Tools5eSources sources = Tools5eSources.findSources(jsonSource);\n        if (sources.filterRuleApplied()) {\n            return false; // keep because a rule says so\n        }\n\n        if (SourceField.reprintedAs.existsIn(jsonSource)) {\n            // This was reprinted in one or more other sources.\n            // If any of those sources have been included, then skip this one\n            // in favor of the (newer) reprint.\n            // \"reprintedAs\": [ \"Deep Gnome|MPMM\" ]\n            // \"reprintedAs\": [\n            //   {\n            //     \"uid\": \"Unarmed Strike|XPHB\",\n            //     \"tag\": \"variantrule\"\n            //   }\n            // ],\n            for (JsonNode reprintedAs : SourceField.reprintedAs.iterateArrayFrom(jsonSource)) {\n                String rawKey = reprintedAs.isObject()\n                        ? SourceField.uid.getTextOrThrow(reprintedAs)\n                        : reprintedAs.asText();\n\n                Tools5eIndexType type = Tools5eIndexType.getTypeFromKey(finalKey);\n                if (reprintedAs.isObject()) {\n                    String tag = SourceField.tag.getTextOrNull(reprintedAs);\n                    if (tag != null) {\n                        type = Tools5eIndexType.fromText(tag);\n                    }\n                }\n\n                // Reprints can also be reprints; follow the alias/reprint chain\n                String reprintKey = getAliasOrDefault(type.fromTagReference(rawKey));\n                if (reprintKey.equals(finalKey)) {\n                    // This may happen specifically for itemtype/property, which have\n                    // arbitrary/fixed key construction\n                    continue;\n\n                }\n                JsonNode reprint = getOriginNoFallback(reprintKey);\n                if (reprint == null) {\n                    if (type == Tools5eIndexType.subrace) {\n                        reprintKey = getAliasOrDefault(Tools5eIndexType.race.fromTagReference(rawKey));\n                        reprint = getOriginNoFallback(reprintKey);\n                    }\n                    if (reprint == null) {\n                        continue;\n                    }\n                }\n\n                Tools5eSources reprintSources = Tools5eSources.findSources(reprint);\n                if (reprintSources.includedByConfig()) {\n                    if (config.reprintBehavior() == ReprintBehavior.edition) {\n                        // Only follow the reprint chain if it's in the same edition\n                        String sourceEdition = sources.edition();\n                        String reprintEdition = reprintSources.edition();\n                        if (reprintEdition != null && sourceEdition != null && !reprintEdition.equals(sourceEdition)) {\n                            tui().logf(Msg.REPRINT, \"(SKIP | edition)   %s: ignoring reprint as %s\",\n                                    finalKey, reprintKey);\n                            continue;\n                        }\n                    }\n                    String lookupKey = finalKey.replace(sources.primarySource().toLowerCase(), \"\");\n                    String versionKey = TtrpgValue.indexVersionKeys.streamFrom(reprint)\n                            .map(x -> x.asText())\n                            .filter(x -> x.startsWith(lookupKey))\n                            .findFirst().orElse(null);\n                    if (versionKey != null) {\n                        reprintKey = versionKey; // more specific version/variant for redirect\n                    }\n\n                    // Otherwise, we have a \"newer\" reprint that should be used instead\n                    tui().logf(Msg.REPRINT, \"(--->| reprinted) %s ==> %s\", finalKey, reprintKey);\n                    // 1) create an alias mapping the old key to the reprinted key\n                    reprints.put(finalKey, reprintKey);\n                    // 2) add the sources of the reprint to the sources of the original (for later linking)\n                    reprintSources.addReprint(sources);\n                    return true;\n                }\n            }\n        }\n        if (SourceField.isReprinted.booleanOrDefault(jsonSource, false)) {\n            tui().logf(Msg.REPRINT, \"(--->| isReprint) %s\", finalKey);\n            return true; // this is a reprint, but we have no alias..\n        }\n        return false; // keep\n    }\n\n    /**\n     * Filter sub-resources based on the inclusion of the parent resource.\n     *\n     * @return true if resource should be kept (not used in a filter)\n     */\n    private boolean processDependentType(final String key) {\n        Tools5eIndexType type = Tools5eIndexType.getTypeFromKey(key);\n        switch (type) {\n            case card -> {\n                String deckKey = Tools5eIndexType.deck.fromChildKey(key);\n                Tools5eSources deckSources = Tools5eSources.findSources(deckKey);\n                return deckSources != null && deckSources.includedByConfig();\n            }\n            case optionalFeatureTypes -> {\n                // optionalFeatureTypes are always included\n                return true;\n            }\n            case classfeature -> {\n                // classfeature is reliably tied to the class\n                //   classfeature|ability score improvement|barbarian|phb|8|phb\n                //   classfeature|ability score improvement|barbarian|xphb|12|xphb\n                String classKey = Tools5eIndexType.classtype.fromChildKey(key);\n                boolean reprinted = reprints.containsKey(classKey);\n                if (!reprinted && Tools5eSources.includedByConfig(classKey)) {\n                    // Only keep the class feature if the parent class is not a reprint.\n                    classFeatures.computeIfAbsent(classKey, k -> new HashSet<>()).add(key);\n                    return true; // keep it\n                }\n            }\n            case subclassFeature -> {\n                // This is where things go sideways\n                // For example, these two versions of a subclass feature exists:\n                //   subclassfeature|zealous presence|barbarian|phb|zealot|xge|10|xge\n                //   subclassfeature|zealous presence|barbarian|xphb|zealot|xphb|10|xphb\n                // usually reachable through the matching subclass\n                //   subclass|path of the zealot|barbarian|phb|xge\n                //   subclass|path of the zealot|barbarian|xphb|xphb\n                // which relies on reprint behavior to resolve, if xphb is around\n                //   subclass|path of the zealot|barbarian|phb|xge -> subclass|path of the zealot|barbarian|xphb|xphb\n                //   subclass|path of the zealot|barbarian|xphb|xge -> subclass|path of the zealot|barbarian|xphb|xphb\n                String scfKey = key;\n                SubclassFeatureKeyData keyData = new SubclassFeatureKeyData(key);\n\n                // does the subclass exist or is it a reprint\n                String scKey = getSubclassKey(keyData.toSubclassKey());\n                boolean scIncluded = Tools5eSources.includedByConfig(scKey);\n                boolean scReprint = reprints.containsKey(scKey);\n\n                if (scReprint) {\n                    // the subclass (including its features) was reprinted.\n                    return false; // remove it\n                }\n\n                // does the parent class exist or is it a reprint\n                String classKey = keyData.toClassKey();\n                boolean classIncluded = Tools5eSources.includedByConfig(classKey);\n                String classReprint = reprints.get(classKey);\n\n                tui().logf(Msg.CLASSES, \"%s\\n\\t(%5s) %s -> %s\\n\\t(%5s) %s -> %s\", key,\n                        classReprint != null, classKey, classReprint,\n                        scReprint, scKey, reprints.get(scKey));\n\n                if (classReprint != null) {\n                    // This is the most common case: PHB -> XPHB\n                    // the reprint behavior will handle this\n                    Tools5eSources altSources = Tools5eSources.findSources(classReprint);\n                    classIncluded = altSources != null && altSources.includedByConfig();\n                    if (!classIncluded) {\n                        return false; // remove it, can't fix it\n                    }\n\n                    // We found the class reprint.\n                    // The reprinted class is the new resource anchor for generated notes\n                    classKey = classReprint;\n\n                    // Change the class source for the subclass feature\n                    keyData.classSource = altSources.primarySource();\n\n                    // is there a subclass key with this new class source?\n                    var altScKey = getSubclassKey(keyData.toSubclassKey());\n                    boolean altScPresent = Tools5eSources.includedByConfig(altScKey);\n                    if (altScPresent) {\n                        // This is the sometimes-covered case:\n                        //   subclass|path of wild magic|barbarian|xphb|tce\n                        // reset all the things to hit the happy path below\n                        scfKey = keyData.toKey();\n                        scKey = altScKey;\n                        classKey = classReprint;\n                        scIncluded = altScPresent;\n                    } else {\n                        // There are times this case is not covered, especially in homebrew, for example:\n                        //   subclassfeature|adamantine hide|druid|phb|forged|exploringeberron|10|exploringeberron\n                        // this subclassfeature is included, but there is no mapping to an xphb version of the subclass.\n                        //\n                        // The reset classKey will force the issue. If the subclass is also present/included,\n                        // then it will be added to the adjusted class.\n                    }\n                }\n\n                if (classIncluded && scIncluded) {\n                    // keep the subclass feature if both the class and subclass are included\n                    subclassMap.computeIfAbsent(classKey, k -> new HashSet<>()).add(scKey);\n                    classFeatures.computeIfAbsent(scKey, k -> new HashSet<>()).add(scfKey);\n                    return true;\n                }\n            }\n            default -> {\n                // no-op\n            }\n        }\n        return false; // remove it!\n    }\n\n    public boolean notPrepared() {\n        return filteredIndex == null;\n    }\n\n    public List<JsonNode> elementsMatching(Tools5eIndexType type, String middle) {\n        String pattern = String.format(\"%s\\\\|%s\\\\|.*\", type, middle)\n                .toLowerCase();\n        return nodesMatching(pattern);\n    }\n\n    private List<JsonNode> nodesMatching(String pattern) {\n        return filteredIndex.entrySet().stream()\n                .filter(e -> e.getKey().matches(pattern))\n                .map(Entry::getValue)\n                .collect(Collectors.toList());\n    }\n\n    private void addSrdEntry(String key, JsonNode node) {\n        if (Tools5eSources.isSrd(node)) {\n            String srdName = Tools5eSources.srdName(node);\n            if (srdName != null) {\n                // If there is a generic/SRD name, replace the specific name in the key\n                String srdKey = key.replace(SourceField.name.getTextOrThrow(node).toLowerCase(),\n                        srdName.toLowerCase());\n                // Add an alias for the srd/generic-form of the name\n                addAlias(srdKey, key);\n                srdKeys.add(srdKey);\n            } else {\n                srdKeys.add(key);\n            }\n        }\n    }\n\n    void addAlias(String key, String alias) {\n        if (key.equals(alias)) {\n            return;\n        }\n        unresolvableKeys.remove(key); // this is now resolvable\n        String old = aliases.putIfAbsent(key, alias);\n        if (old != null && !old.equals(alias)) {\n            tui().warnf(\"Oops! Duplicate simple key: %s; old: %s; new: %s\", key, old, alias);\n        }\n    }\n\n    public String getSubclassKey(String targetKey) {\n        // short name to long name without following reprints.\n        return getAliasOrDefault(targetKey, false);\n    }\n\n    public List<String> getAliasesFor(String targetKey) {\n        return aliases.entrySet().stream()\n                .filter(e -> e.getValue().equals(targetKey))\n                .map(Entry::getKey)\n                .collect(Collectors.toList());\n    }\n\n    public String getAliasOrDefault(String key) {\n        if (!isPresent(key)) {\n            return null;\n        }\n        return getAliasOrDefault(key, true);\n    }\n\n    public String getAliasOrDefault(String key, boolean includeReprints) {\n        String previous;\n        String value = key;\n        do {\n            previous = value;\n\n            // race -> possible subrace alias\n            String alias = value.startsWith(\"race\")\n                    ? subraceMap.get(previous)\n                    : null;\n\n            if (includeReprints) {\n                String reprint = reprints.get(alias == null ? previous : alias);\n                if (reprint != null) {\n                    alias = reprint;\n                }\n            }\n\n            value = alias == null\n                    ? aliases.getOrDefault(previous, previous)\n                    : alias;\n        } while (!value.equals(previous));\n        return value;\n    }\n\n    /**\n     * For subclasses, class features, and subclass features,\n     * cross references come directly from the class definition\n     * (as a lookup for additional json sources).\n     *\n     * @param finalKey Pre-created cross reference string (including type)\n     * @return referenced JsonNode or null\n     */\n    public JsonNode getNode(String finalKey) {\n        if (finalKey == null || finalKey.isEmpty()) {\n            return null;\n        }\n        return filteredIndex.get(finalKey);\n    }\n\n    public Collection<HomebrewMetaTypes> getHomebrewMetaTypes(Tools5eSources activeSources) {\n        return homebrewIndex.getHomebrewMetaTypes(activeSources);\n    }\n\n    public ItemProperty findItemProperty(String tagReference, Tools5eSources sources) {\n        if (!isPresent(tagReference)) {\n            return null;\n        }\n        String key = ItemProperty.refTagToKey(tagReference);\n        ItemProperty itemProperty = ItemProperty.forKey(tagReference);\n        if (itemProperty == null) {\n            JsonNode propertyNode = getOriginNoFallback(getAliasOrDefault(key));\n            if (propertyNode != null) {\n                itemProperty = ItemProperty.fromNode(propertyNode);\n            }\n            if (itemProperty == null) {\n                // try homebrew (normalize from key)\n                String[] parts = key.split(\"\\\\|\");\n                itemProperty = homebrewIndex.findHomebrewProperty(parts[1], sources);\n\n                if (itemProperty != null) {\n                    // add alias for resolved property\n                    String itemKey = itemProperty.indexKey();\n                    addAlias(key, itemKey);\n                }\n            }\n        }\n        return itemProperty;\n    }\n\n    public ItemType findItemType(String tagReference, Tools5eSources sources) {\n        if (!isPresent(tagReference)) {\n            return null;\n        }\n        String key = ItemType.refTagToKey(tagReference);\n        ItemType itemType = ItemType.forKey(key);\n        if (itemType == null) {\n            JsonNode typeNode = getOriginNoFallback(getAliasOrDefault(key));\n            if (typeNode != null) {\n                itemType = ItemType.fromNode(typeNode);\n            }\n            if (itemType == null) {\n                // try homebrew (normalize from key)\n                String[] parts = key.split(\"\\\\|\");\n                itemType = homebrewIndex.findHomebrewType(parts[1], sources);\n\n                if (itemType != null) {\n                    // add alias for resolved item type\n                    String itemKey = itemType.indexKey();\n                    addAlias(key, itemKey);\n                }\n            }\n        }\n        return itemType;\n    }\n\n    public ItemMastery findItemMastery(String tagReference, Tools5eSources sources) {\n        if (!isPresent(tagReference)) {\n            return null;\n        }\n        // This is always a tag: name|source\n        String key = Tools5eIndexType.itemMastery.fromTagReference(tagReference);\n        ItemMastery mastery = ItemMastery.forKey(key);\n        if (mastery == null) {\n            JsonNode masteryNode = getOriginNoFallback(getAliasOrDefault(key));\n            if (masteryNode != null) {\n                mastery = ItemMastery.fromNode(masteryNode);\n            }\n            if (mastery == null) {\n                // try homebrew (normalize from key)\n                String[] parts = key.split(\"\\\\|\");\n                mastery = homebrewIndex.findHomebrewMastery(parts[1], sources);\n                if (mastery != null) {\n                    // add alias for resolved item mastery\n                    String itemKey = mastery.indexKey();\n                    addAlias(key, itemKey);\n                }\n            }\n        }\n        return mastery;\n    }\n\n    public SkillOrAbility findSkillOrAbility(String key, Tools5eSources sources) {\n        if (!isPresent(key)) {\n            return null;\n        }\n        SkillOrAbility skill = resolvedSkills.computeIfAbsent(key, k -> {\n            SkillOrAbility sk = SkillOrAbility.fromTextValue(key);\n            if (sk == null) {\n                sk = homebrewIndex.findHomebrewSkillOrAbility(key, sources);\n                if (sk == null) {\n                    tui().warnf(Msg.UNKNOWN, \"Unknown skill or ability %s in %s\", key, sources);\n                    return new CustomSkillOrAbility(key);\n                }\n            }\n            return sk;\n        });\n        return skill;\n    }\n\n    public SpellSchool findSpellSchool(String code, Tools5eSources sources) {\n        if (code == null || code.isEmpty()) {\n            return SpellSchool.SchoolEnum.None;\n        }\n        SpellSchool school = SpellSchool.fromEncodedValue(code);\n        if (school == null) {\n            school = homebrewIndex.findHomebrewSpellSchool(code, sources);\n        }\n        if (school == null) {\n            tui().warnf(Msg.UNKNOWN, \"Unknown spell school %s in %s\", code, sources);\n            return new CustomSpellSchool(code, code);\n        }\n        return school;\n    }\n\n    public Set<String> findSubclasses(String classKey) {\n        return subclassMap.getOrDefault(classKey, Set.of());\n    }\n\n    /**\n     * Resolve the effective class source through reprints.\n     * If the class identified by className+classSource was reprinted\n     * (e.g., PHB Druid → XPHB Druid), return the reprint target's source.\n     */\n    public String resolveClassSource(String className, String classSource) {\n        String classKey = String.join(\"|\",\n                Tools5eIndexType.classtype.name(),\n                className, classSource).toLowerCase();\n        String reprint = reprints.get(classKey);\n        if (reprint != null) {\n            Tools5eSources reprintSources = Tools5eSources.findSources(reprint);\n            if (reprintSources != null && reprintSources.includedByConfig()) {\n                return reprintSources.primarySource();\n            }\n        }\n        return classSource;\n    }\n\n    public Set<String> findClassFeatures(String classOrSubclassKey) {\n        return classFeatures.getOrDefault(classOrSubclassKey, Set.of());\n    }\n\n    public JsonNode findTable(SourceAndPage sourceAndPage, String rowData) {\n        List<JsonNode> tables = tableIndex.get(sourceAndPage);\n        if (tables != null) {\n            if (tables.size() == 1) {\n                Optional<JsonNode> table = matchTable(rowData, tables.get(0));\n                return table.orElse(null);\n            }\n            for (JsonNode table : tables) {\n                Optional<JsonNode> match = matchTable(rowData, table);\n                if (match.isPresent()) {\n                    return match.get();\n                }\n            }\n        }\n        return null;\n    }\n\n    private Optional<JsonNode> matchTable(String rowData, JsonNode table) {\n        String matchData = TableFields.getFirstRow(table);\n        if (rowData.equals(matchData)) {\n            return Optional.of(table);\n        }\n        return Optional.empty();\n    }\n\n    public JsonNode getOriginNoFallback(String finalKey) {\n        JsonNode result = nodeIndex.get(finalKey);\n        if (result == null) {\n            // subraces are initially held in a separate map; check there (handle copies)\n            result = subraces.get(finalKey);\n            if (result == null && unresolvableKeys.add(finalKey)) {\n                tui().logf(Msg.UNRESOLVED, \"No element found for %s\", finalKey);\n            }\n        }\n        return result;\n    }\n\n    public JsonNode getOrigin(String finalKey) {\n        if (unresolvableKeys.contains(finalKey)) {\n            return null;\n        }\n\n        JsonNode result = nodeIndex.get(finalKey);\n        if (result == null) {\n            List<String> target = nodeIndex.keySet().stream()\n                    .filter(k -> k.startsWith(finalKey))\n                    .collect(Collectors.toList());\n            if (target.size() == 1) {\n                String lookup = target.get(0);\n                result = nodeIndex.get(lookup);\n            } else if (target.size() > 1) {\n                List<String> reduce = target.stream()\n                        .filter(x -> !x.matches(\".*\\\\|ua[^|]*$\"))\n                        .filter(x -> !x.contains(\"|dmg\"))\n                        .filter(x -> isIncluded(x))\n                        .distinct()\n                        .collect(Collectors.toList());\n                if (reduce.size() > 1) {\n                    tui().debugf(Msg.MULTIPLE, \"Found several elements for %s: %s\",\n                            finalKey, reduce);\n                    return null;\n                } else if (reduce.size() == 1) {\n                    String lookup = reduce.get(0);\n                    result = nodeIndex.get(lookup);\n                }\n            }\n            if (result == null && unresolvableKeys.add(finalKey)) {\n                tui().logf(Msg.UNRESOLVED, \"No element found for %s\", finalKey);\n            }\n        }\n        return result;\n    }\n\n    public String linkifyByName(Tools5eIndexType type, String name) {\n        String prefix = String.format(\"%s|%s|\", type, name).toLowerCase();\n\n        return nameToLink.computeIfAbsent(prefix, p -> {\n            // Akin to getAliasOrDefault, but we have to filter by prefix\n            List<String> target = List.of();\n\n            if (type == Tools5eIndexType.subrace || type == Tools5eIndexType.race) {\n                target = subraceMap.keySet().stream()\n                        .filter(k -> k.startsWith(prefix))\n                        .collect(Collectors.toList());\n            }\n\n            if (target.isEmpty()) {\n                target = reprints.keySet().stream()\n                        .filter(k -> k.startsWith(prefix))\n                        .collect(Collectors.toList());\n            }\n\n            if (target.isEmpty()) {\n                target = aliases.keySet().stream()\n                        .filter(k -> k.startsWith(prefix))\n                        .collect(Collectors.toList());\n            }\n\n            if (target.isEmpty()) {\n                target = nodeIndex.keySet().stream()\n                        .filter(k -> k.startsWith(prefix))\n                        .collect(Collectors.toList());\n            }\n\n            if (target.isEmpty()) {\n                tui().debugf(Msg.UNRESOLVED, \"linkifyByName: unresolved element for \\\"%s\\\" using [%s]\", name, prefix);\n                return name;\n            } else if (target.size() > 1) {\n                List<String> reduce = target.stream()\n                        .filter(x -> !x.matches(\".*\\\\|ua[^|]*$\"))\n                        .map(x -> getAliasOrDefault(x))\n                        .filter(x -> isIncluded(x))\n                        .distinct()\n                        .collect(Collectors.toList());\n                if (reduce.size() > 1) {\n                    tui().debugf(Msg.MULTIPLE, \"Found several elements for %s using [%s]: %s\",\n                            name, prefix, target);\n                    return name;\n                } else if (reduce.size() == 1) {\n                    target = reduce;\n                }\n            }\n\n            String key = getAliasOrDefault(target.get(0));\n            JsonNode node = filteredIndex.get(key); // only included items\n            return node == null ? name : type.linkify(this, node);\n        });\n    }\n\n    public boolean customContentIncluded() {\n        // The biggest hack of all time (not really).\n        // I have some custom content for types/property/mastery that\n        // should be included, but only if some combination of\n        // basic/free rules, srd, phb or dmg is included\n        return Tools5eIndex.isSrdBasicOnly()\n                || config.sourcesIncluded(List.of(\"phb\", \"dmg\", \"xphb\", \"xdmg\"));\n    }\n\n    public boolean sourceIncluded(String source) {\n        return config.sourceIncluded(source);\n    }\n\n    public boolean isIncluded(String key) {\n        String alias = getAliasOrDefault(key);\n        return filteredIndex.containsKey(key) || filteredIndex.containsKey(alias);\n    }\n\n    public boolean isIncluded(String key, boolean followReprints) {\n        String alias = getAliasOrDefault(key, followReprints);\n        return filteredIndex.containsKey(key) || filteredIndex.containsKey(alias);\n    }\n\n    public boolean isExcluded(String key) {\n        return !isIncluded(key);\n    }\n\n    public void recordTableReference(String key) {\n        referencedTableKeys.add(key);\n    }\n\n    public boolean isTableReferenced(String key) {\n        return referencedTableKeys.contains(key);\n    }\n\n    public boolean differentSource(Tools5eSources sources, String source) {\n        String primarySource = sources == null ? null : sources.primarySource();\n        if (primarySource == null || source == null) {\n            return false;\n        }\n        return !primarySource.equals(source);\n    }\n\n    public Set<Entry<String, JsonNode>> includedEntries() {\n        if (notPrepared()) {\n            throw new IllegalStateException(\"Index must be prepared before writing indexes\");\n        }\n        return filteredIndex.entrySet();\n    }\n\n    public OptionalFeatureType getOptionalFeatureType(JsonNode optfeatureNode) {\n        return optFeatureIndex.get(optfeatureNode);\n    }\n\n    public OptionalFeatureType getOptionalFeatureType(String featureType) {\n        if (featureType == null) {\n            return null;\n        }\n        return optFeatureIndex.get(featureType);\n    }\n\n    @Override\n    public void writeFullIndex(Path outputFile) throws IOException {\n        if (notPrepared()) {\n            throw new IllegalStateException(\"Index must be prepared before writing indexes\");\n        }\n        Map<String, Object> allKeys = new LinkedHashMap<>();\n        allKeys.put(\"keys\", nodeIndex.keySet());\n        allKeys.put(\"mapping\", aliases);\n        allKeys.put(\"reprints\", reprints);\n        allKeys.put(\"subraceMap\", subraceMap);\n        allKeys.put(\"subclassMap\", subclassMap);\n        allKeys.put(\"classFeatures\", classFeatures);\n        allKeys.put(\"optionalFeatures\", optFeatureIndex.getMap());\n        allKeys.put(\"srdKeys\", srdKeys);\n        allKeys.put(\"unresolvableKeys\", unresolvableKeys);\n        tui().writeJsonFile(outputFile, allKeys);\n    }\n\n    @Override\n    public void writeFilteredIndex(Path outputFile) throws IOException {\n        if (notPrepared()) {\n            throw new IllegalStateException(\"Index must be prepared before writing files\");\n        }\n        tui().writeJsonFile(outputFile, Map.of(\"keys\", new TreeSet<>(filteredIndex.keySet())));\n    }\n\n    @Override\n    public JsonNode getAdventure(String id) {\n        String finalKey = Tools5eIndexType.adventure.createKey(\"adventure\", id);\n        return getNode(finalKey); // filtered\n    }\n\n    @Override\n    public JsonNode getBook(String id) {\n        String finalKey = Tools5eIndexType.book.createKey(\"book\", id);\n        return getNode(finalKey); // filtered\n    }\n\n    void linkSources(Tools5eIndexType type, JsonNode dataNode) {\n        String id = dataNode.get(\"id\").asText();\n\n        String finalKey = type == Tools5eIndexType.adventureData\n                ? Tools5eIndexType.adventure.createKey(\"adventure\", id)\n                : Tools5eIndexType.book.createKey(\"book\", id);\n\n        JsonNode fromNode = getOrigin(finalKey);\n\n        // Adventures and Books have metadata in a different entry.\n        SourceField.name.link(fromNode, dataNode);\n        SourceField.source.link(fromNode, dataNode);\n        SourceField.page.link(fromNode, dataNode);\n        Tools5eFields.otherSources.link(fromNode, dataNode);\n        Tools5eFields.additionalSources.link(fromNode, dataNode);\n    }\n\n    @Override\n    public MarkdownConverter markdownConverter(MarkdownWriter writer) {\n        return new Tools5eMarkdownConverter(this, writer);\n    }\n\n    @Override\n    public CompendiumConfig cfg() {\n        return this.config;\n    }\n\n    @Override\n    public Tools5eIndex index() {\n        return this;\n    }\n\n    @Override\n    public Tools5eSources getSources() {\n        return null;\n    }\n\n    public SpellIndex getSpellIndex() {\n        return spellIndex;\n    }\n\n    @Override\n    public String toString() {\n        return \"Tools5eIndex{\" +\n                \"nodeIndex=\" + nodeIndex.size() +\n                \", filteredIndex=\" + (filteredIndex == null ? 0 : filteredIndex.size()) +\n                \", aliases=\" + aliases.size() +\n                \", reprints=\" + reprints.size() +\n                \", subraceMap=\" + subraceMap.size() +\n                \", subclassMap=\" + subclassMap.size() +\n                \", classFeatures=\" + classFeatures.size() +\n                \", optFeatureIndex=\" + optFeatureIndex.getMap().size() +\n                \", srdKeys=\" + srdKeys.size() +\n                '}';\n    }\n\n    public void cleanup() {\n        if (instance == this) {\n            instance = null;\n        }\n        nodeIndex.clear();\n        subraces.clear();\n        tableIndex.clear();\n        legendaryGroupMonsters.clear();\n        referencedTableKeys.clear();\n\n        if (filteredIndex != null) {\n            filteredIndex.clear();\n        }\n\n        aliases.clear();\n        reprints.clear();\n        subraceMap.clear();\n        nameToLink.clear();\n\n        srdKeys.clear();\n\n        spellIndex.clear();\n        optFeatureIndex.clear();\n        homebrewIndex.clear();\n\n        // affiliated sources cache, too\n        Tools5eSources.clear();\n\n        ItemMastery.clear();\n        ItemProperty.clear();\n        ItemType.clear();\n    }\n\n    static class Tuple {\n        final String key;\n        final JsonNode node;\n\n        public Tuple(String key, JsonNode node) {\n            this(key, node, null);\n        }\n\n        public Tuple(String key, JsonNode node, String name) {\n            this.key = key;\n            this.node = node;\n            this.name = name;\n        }\n\n        String name;\n\n        public String getName() {\n            if (name == null) {\n                name = node.get(\"name\").asText();\n            }\n            return name;\n        }\n\n        String source;\n\n        public String getSource() {\n            if (source == null) {\n                source = node.get(\"source\").asText();\n            }\n            return source;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eIndexType.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static dev.ebullient.convert.StringUtil.isPresent;\nimport static dev.ebullient.convert.StringUtil.valueOrDefault;\n\nimport java.util.function.BiConsumer;\nimport java.util.stream.Stream;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.config.TtrpgConfig;\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.tools.IndexType;\nimport dev.ebullient.convert.tools.JsonNodeReader;\nimport dev.ebullient.convert.tools.JsonTextConverter.SourceField;\nimport dev.ebullient.convert.tools.ToolsIndex.TtrpgValue;\n\npublic enum Tools5eIndexType implements IndexType, JsonNodeReader {\n    action,\n    adventure,\n    adventureData,\n    background,\n    backgroundFluff,\n    book,\n    bookData,\n    boon,\n    card,\n    charoption,\n    charoptionFluff,\n    citation,\n    classtype(\"class\"),\n    classFluff,\n    classfeature,\n    condition,\n    conditionFluff,\n    cult,\n    disease,\n    deity,\n    deck,\n    facility(\"bastion\"),\n    facilityFluff,\n    feat,\n    featFluff,\n    hazard,\n    hazardFluff,\n    item,\n    itemEntry,\n    itemFluff,\n    itemGroup,\n    itemMastery,\n    itemProperty,\n    itemType,\n    itemTypeAdditionalEntries,\n    language,\n    languageFluff,\n    legendaryGroup,\n    magicvariant,\n    monster,\n    monsterFluff,\n    monsterfeatures,\n    monsterTemplate,\n    object,\n    objectFluff,\n    optfeature,\n    optionalFeatureTypes, // homebrew\n    optionalfeatureFluff,\n    psionic,\n    psionicTypes, // homebrew\n    race,\n    raceFeature,\n    raceFluff,\n    reward,\n    rewardFluff,\n    sense,\n    skill,\n    spell,\n    spellFluff,\n    spellSchool, // homebrew\n    status,\n    subclass,\n    subclassFeature,\n    subclassFluff,\n    subrace(\"race\"),\n    table,\n    tableGroup,\n    trap,\n    trapFluff,\n    variantrule,\n    vehicle,\n    vehicleFluff,\n    vehicleUpgrade,\n\n    note, // qute data type\n    reference, // made up\n    syntheticGroup, // qute data type\n    spellIndex, // made up\n    ;\n\n    final String templateName;\n\n    Tools5eIndexType() {\n        this.templateName = this.name();\n    }\n\n    Tools5eIndexType(String templateName) {\n        this.templateName = templateName;\n    }\n\n    public String templateName() {\n        return templateName;\n    }\n\n    public static Tools5eIndexType fromText(String name) {\n        if (\"creature\".equalsIgnoreCase(name)) {\n            return monster;\n        }\n        if (\"creatureFluff\".equalsIgnoreCase(name)) {\n            return monsterFluff;\n        }\n        if (\"optionalfeature\".equalsIgnoreCase(name)) {\n            return optfeature;\n        }\n        if (\"legroup\".equalsIgnoreCase(name)) {\n            return legendaryGroup;\n        }\n        return Stream.of(values())\n                .filter(x -> x.templateName.equalsIgnoreCase(name) || x.name().equalsIgnoreCase(name))\n                .findFirst().orElse(null);\n    }\n\n    public static Tools5eIndexType getTypeFromKey(String key) {\n        if (!isPresent(key)) {\n            return null;\n        }\n        String typeKey = key.substring(0, key.indexOf(\"|\"));\n        return fromText(typeKey);\n    }\n\n    public static Tools5eIndexType getTypeFromNode(JsonNode node) {\n        String typeKey = TtrpgValue.indexInputType.getTextOrEmpty(node);\n        return fromText(typeKey);\n    }\n\n    @Override\n    public String createKey(JsonNode x) {\n        if (this == book || this == adventure || this == bookData || this == adventureData) {\n            String id = SourceField.id.getTextOrEmpty(x);\n            return String.format(\"%s|%s-%s\",\n                    this.name(),\n                    this.name().replace(\"Data\", \"\"),\n                    id).toLowerCase();\n        } else if (this == itemTypeAdditionalEntries) {\n            return createKey(\n                    IndexFields.appliesTo.getTextOrEmpty(x),\n                    SourceField.source.getTextOrEmpty(x));\n        }\n\n        String name = SourceField.name.getTextOrEmpty(x).trim();\n        String source = SourceField.source.getTextOrEmpty(x).trim();\n\n        // With introduction of XPHB, etc., we are going to be explicit about sources\n        // links will be adjusted to add assumed sources\n        return switch (this) {\n            case classfeature -> {\n                String classSource = IndexFields.classSource.getTextOrDefault(x, \"phb\");\n                yield \"%s|%s|%s|%s|%s|%s\".formatted(\n                        this.name(),\n                        name,\n                        IndexFields.className.getTextOrEmpty(x),\n                        classSource,\n                        IndexFields.level.getTextOrEmpty(x),\n                        source)\n                        .toLowerCase();\n            }\n            case card -> {\n                String set = IndexFields.set.getTextOrThrow(x).trim();\n                yield \"%s|%s|%s|%s\".formatted(\n                        this.name(),\n                        name,\n                        set,\n                        source)\n                        .toLowerCase();\n            }\n            case deity -> {\n                yield \"%s|%s|%s|%s\".formatted(\n                        this.name(),\n                        name,\n                        IndexFields.pantheon.getTextOrEmpty(x).trim(),\n                        source)\n                        .toLowerCase();\n            }\n            case itemType, itemProperty -> {\n                source = SourceField.source.getTextOrDefault(x, \"phb\");\n                String abbreviation = IndexFields.abbreviation.getTextOrDefault(x, name).trim();\n                yield \"%s|%s|%s\".formatted(\n                        this.name(),\n                        abbreviation,\n                        source)\n                        .toLowerCase();\n            }\n            case itemEntry -> {\n                yield \"%s|%s|%s\".formatted(\n                        this.name(),\n                        name,\n                        source)\n                        .toLowerCase();\n            }\n            case optfeature -> {\n                yield \"%s|%s|%s\".formatted(\n                        this.name(),\n                        name,\n                        source)\n                        .toLowerCase();\n            }\n            case reference -> {\n                if (!isPresent(source)) {\n                    source = Tools5eSources.has2024Content()\n                            ? \"XPHB\"\n                            : \"PHB\";\n                }\n                yield createKey(name, source);\n            }\n            case subclass -> {\n                String classSource = IndexFields.classSource.getTextOrDefault(x, \"phb\");\n                String scSource = SourceField.source.getTextOrDefault(x, classSource);\n                // subclass|subclassName|className|classSource|subclassSource\n                yield \"%s|%s|%s|%s|%s\".formatted(\n                        this.name(),\n                        name,\n                        IndexFields.className.getTextOrEmpty(x).trim(),\n                        classSource,\n                        scSource)\n                        .toLowerCase();\n            }\n            case subclassFeature -> {\n                String classSource = IndexFields.classSource.getTextOrDefault(x, \"phb\");\n                String scSource = IndexFields.subclassSource.getTextOrDefault(x, \"phb\");\n                // scFeature|className|classSource|subclassShortName|subclassSource|level|source\n                yield \"%s|%s|%s|%s|%s|%s|%s|%s\".formatted(\n                        this.name(),\n                        name,\n                        IndexFields.className.getTextOrEmpty(x).trim(),\n                        classSource,\n                        IndexFields.subclassShortName.getTextOrEmpty(x).trim(),\n                        scSource,\n                        IndexFields.level.getTextOrEmpty(x),\n                        source)\n                        .toLowerCase();\n            }\n            case subrace -> {\n                String raceSource = IndexFields.raceSource.getTextOrDefault(x, \"phb\");\n                yield \"%s|%s|%s|%s|%s\".formatted(\n                        this.name(),\n                        name,\n                        IndexFields.raceName.getTextOrEmpty(x).trim(),\n                        raceSource,\n                        source)\n                        .toLowerCase();\n            }\n            default -> createKey(name, source);\n        };\n    }\n\n    public String createKey(String name, String source) {\n        if (source == null) {\n            return String.format(\"%s|%s\", this.name(), name).toLowerCase();\n        }\n        return switch (this) {\n            case adventure,\n                    adventureData,\n                    book,\n                    bookData ->\n                String.format(\"%s|%s-%s\", this.name(), name, source).toLowerCase();\n            default ->\n                String.format(\"%s|%s|%s\", this.name(), name, source).toLowerCase();\n        };\n    }\n\n    public String fromTagReference(String crossRef) {\n        if (crossRef == null || crossRef.isEmpty()) {\n            return null;\n        }\n        String[] parts = crossRef.trim().split(\"\\s?\\\\|\\\\s?\");\n        return switch (this) {\n            case card -> {\n                // 0    name,\n                // 1    set,\n                // 2    source\n                yield String.format(\"%s|%s|%s|%s\",\n                        this.name(),\n                        parts[0].trim(),\n                        parts[1].trim(),\n                        valueOrDefault(parts, 2, defaultSourceString()))\n                        .toLowerCase();\n            }\n            case classfeature -> {\n                // 0    name,\n                // 1    IndexFields.className.getTextOrEmpty(x),\n                // 2    classSource || \"phb\",\n                // 3    IndexFields.level.getTextOrEmpty(x),\n                // 4    source || classSource\n                if (parts.length < 4) {\n                    Tui.instance().errorf(\"Badly formed Class Feature key (not enough segments): %s\", crossRef);\n                    yield null;\n                }\n                String classSource = valueOrDefault(parts, 2, Tools5eIndexType.classtype.defaultSourceString());\n                String featureSource = valueOrDefault(parts, 4, classSource);\n                yield \"%s|%s|%s|%s|%s|%s\".formatted(this.name(),\n                        parts[0].trim(),\n                        parts[1].trim(),\n                        classSource,\n                        parts[3].trim(),\n                        featureSource)\n                        .toLowerCase();\n            }\n            case classtype -> {\n                // A {@class} tag can reference either a class or a subclass.\n                // {@class fighter|phb|optional link text added with another pipe}\n                // {@class Fighter|phb|Samurai|Samurai|xge}\n                // {@subclass} tags have a different structure\n                if (parts.length < 5) {\n                    yield \"%s|%s|%s\".formatted(\n                            this.name(),\n                            parts[0].trim(),\n                            valueOrDefault(parts, 1, defaultSourceString()))\n                            .toLowerCase();\n                }\n                yield getSubclassKey(\n                        parts[0],\n                        valueOrDefault(parts, 1, defaultSourceString()),\n                        valueOrDefault(parts, 3, null),\n                        valueOrDefault(parts, 4, defaultSourceString()));\n            }\n            case deity -> {\n                yield \"%s|%s|%s|%s\".formatted(\n                        this.name(),\n                        parts[0],\n                        valueOrDefault(parts, 1, \"Forotten Realms\"),\n                        valueOrDefault(parts, 2, defaultSourceString()))\n                        .toLowerCase();\n            }\n            case itemProperty -> {\n                yield ItemProperty.refTagToKey(crossRef);\n            }\n            case itemType -> {\n                yield ItemType.refTagToKey(crossRef);\n            }\n            case subclass -> {\n                // Homebrew and reprint tags\n                // {@subclass Artillerist|Artificer|TCE|TCE}\n                // 0    subclassShortName,\n                // 1    IndexFields.className.getTextOrEmpty(x),\n                // 2    classSource || \"phb\",\n                // 3    subClassSource || \"phb\"\n                if (parts.length < 2) {\n                    Tui.instance().errorf(\"Badly formed Subclass key (not enough segments): %s\", crossRef);\n                    yield null;\n                }\n                String scName = parts[0];\n                String className = parts[1];\n                String classSource = valueOrDefault(parts, 2, \"phb\");\n                String subClassSource = valueOrDefault(parts, 3, \"phb\");\n\n                yield getSubclassKey(\n                        className, classSource,\n                        scName, subClassSource);\n            }\n            case subclassFeature -> {\n                // 0    name,\n                // 1    IndexFields.className.getTextOrEmpty(x),\n                // 2    classSource || \"phb\",\n                // 3    IndexFields.subclassShortName.getTextOrEmpty(x),\n                // 4    subClassSource || \"phb\",\n                // 5    IndexFields.level.getTextOrEmpty(x),\n                // 6    source || subClassSource\n                if (parts.length < 6) {\n                    Tui.instance().errorf(\"Badly formed Subclass Feature key (not enough segments): %s\", crossRef);\n                    yield null;\n                }\n                String classSource = valueOrDefault(parts, 2, \"phb\");\n                String scSource = valueOrDefault(parts, 4, \"phb\");\n                String featureSource = valueOrDefault(parts, 6, scSource);\n\n                yield \"%s|%s|%s|%s|%s|%s|%s|%s\".formatted(\n                        Tools5eIndexType.subclassFeature,\n                        parts[0],\n                        parts[1].trim(),\n                        classSource,\n                        parts[3].trim(),\n                        scSource,\n                        parts[5].trim(),\n                        featureSource)\n                        .toLowerCase();\n            }\n            default -> {\n                // 0    name,\n                // 1    source\n                yield createKey(parts[0],\n                        valueOrDefault(parts, 1, defaultSourceString()));\n            }\n        };\n    }\n\n    public String toTagReference(JsonNode entry) {\n        String linkText = Tools5eLinkifier.instance().decoratedName(this, entry);\n        String name = SourceField.name.getTextOrEmpty(entry);\n        String source = SourceField.source.getTextOrEmpty(entry);\n\n        return switch (this) {\n            // {@card Donjon|Deck of Several Things|LLK}\n            case card -> \"%s|%s|%s\".formatted(\n                    name,\n                    IndexFields.deck.getTextOrEmpty(entry),\n                    source);\n            // {@subclass Artillerist|Artificer|TCE|TCE}\n            case subclass -> \"%s|%s|%s|%s|%s\".formatted(\n                    name,\n                    IndexFields.className.getTextOrEmpty(entry),\n                    IndexFields.classSource.getTextOrEmpty(entry),\n                    source,\n                    linkText);\n            // {@subclassFeature Blessed Strikes|Cleric|PHB|Twilight|TCE|8|TCE}\n            case subclassFeature -> \"%s|%s|%s|%s|%s|%s|%s|%s\".formatted(\n                    name,\n                    IndexFields.className.getTextOrEmpty(entry),\n                    IndexFields.classSource.getTextOrEmpty(entry),\n                    IndexFields.subclassShortName.getTextOrEmpty(entry),\n                    IndexFields.subclassSource.getTextOrEmpty(entry),\n                    IndexFields.level.getTextOrEmpty(entry),\n                    source,\n                    linkText);\n            // {@itemType abv|source|linkText}\n            case itemProperty, itemType -> \"%s|%s|%s\".formatted(\n                    IndexFields.abbreviation.getTextOrEmpty(entry),\n                    source, linkText);\n            // {@feat name|source|linkText}\n            default -> \"%s|%s|%s\".formatted(name, source, linkText);\n        };\n    }\n\n    public String linkify(JsonSource convert, JsonNode entry) {\n        String reference = toTagReference(entry);\n        return convert.linkify(this, reference);\n    }\n\n    public static String getSubclassKey(String className, String classSource, String subclassName, String subclassSource) {\n        classSource = valueOrDefault(classSource, Tools5eIndexType.classtype.defaultSourceString());\n        subclassSource = valueOrDefault(subclassSource, Tools5eIndexType.subclass.defaultSourceString());\n        return \"%s|%s|%s|%s|%s\".formatted(\n                Tools5eIndexType.subclass,\n                subclassName,\n                className,\n                classSource,\n                subclassSource)\n                .toLowerCase();\n    }\n\n    public String fromChildKey(String key) {\n        if (!isPresent(key)) {\n            return null;\n        }\n        return switch (this) {\n            case deck, classtype, race -> {\n                String[] parts = key.trim().split(\"\\s?\\\\|\\\\s?\");\n                // card|cardName|deckName|source\n                // classfeature|cfName|className|classSource|level|cfSource\n                // subclass|scName|className|classSource|scSource\n                // subclassfeature|scfName|className|classSource|subclassShortName|scSource|level|scfSource\n                // subrace|subraceName|raceName|raceSource|subraceSource\n                yield parts.length < 4 ? null : \"%s|%s|%s\".formatted(this, parts[2], parts[3]);\n            }\n            case subclass -> {\n                String[] parts = key.trim().split(\"\\s?\\\\|\\\\s?\");\n                // subclassfeature|scfName|className|classSource|subclassShortName|scSource|level|scfSource\n                yield parts.length < 6 ? null : \"%s|%s|%s|%s|%s\".formatted(this, parts[4], parts[2], parts[3], parts[5]);\n            }\n            default -> null;\n        };\n    }\n\n    public boolean multiNode() {\n        return switch (this) {\n            case action,\n                    condition,\n                    disease,\n                    itemType,\n                    itemProperty,\n                    itemMastery,\n                    sense,\n                    skill,\n                    spellIndex,\n                    status,\n                    syntheticGroup ->\n                true;\n            default -> false;\n        };\n    }\n\n    public boolean writeFile() {\n        return switch (this) {\n            case background,\n                    classtype,\n                    deck,\n                    deity,\n                    facility,\n                    feat,\n                    hazard,\n                    item,\n                    itemGroup,\n                    monster,\n                    object,\n                    optfeature,\n                    psionic,\n                    race,\n                    subrace,\n                    reward,\n                    spell,\n                    trap,\n                    vehicle ->\n                true;\n            default -> false;\n        };\n    }\n\n    public boolean useQuteNote() {\n        return switch (this) {\n            case action,\n                    adventureData,\n                    bookData,\n                    condition,\n                    disease,\n                    itemType,\n                    itemProperty,\n                    itemMastery,\n                    legendaryGroup,\n                    optionalFeatureTypes,\n                    sense,\n                    skill,\n                    spellIndex,\n                    status,\n                    table,\n                    tableGroup,\n                    variantrule ->\n                true; // QuteNote-based\n            default -> false; // QuteBase\n        };\n    }\n\n    public boolean useCompendiumBase() {\n        return switch (this) {\n            case action,\n                    condition,\n                    disease,\n                    itemProperty,\n                    itemType,\n                    itemMastery,\n                    sense,\n                    skill,\n                    status,\n                    variantrule ->\n                false; // use rules\n            default -> true; // use compendium\n        };\n    }\n\n    // render.js -- Tag*\n    public String defaultSourceString() {\n        return switch (this) {\n            case card,\n                    deck,\n                    disease,\n                    hazard,\n                    item,\n                    itemGroup,\n                    magicvariant,\n                    object,\n                    reward,\n                    table,\n                    tableGroup,\n                    trap,\n                    variantrule ->\n                \"DMG\";\n            case legendaryGroup,\n                    monster,\n                    monsterfeatures ->\n                \"MM\";\n            case boon, cult -> \"MTF\";\n            case charoption -> \"MOT\";\n            case facility -> \"XDMG\";\n            case itemMastery -> \"XPHB\";\n            case itemTypeAdditionalEntries -> \"XGE\";\n            case psionic -> \"UATheMysticClass\";\n            case vehicle, vehicleUpgrade -> \"GoS\";\n            // ---\n            case syntheticGroup -> null;\n            case reference ->\n                Tools5eSources.has2024Content()\n                        ? \"XPHB\"\n                        : \"PHB\";\n            default -> \"PHB\";\n        };\n    }\n\n    public String defaultOutputSource() {\n        return switch (this) {\n            case classtype, classfeature, subclass, subclassFeature ->\n                TtrpgConfig.getDefaultOutputSource(classtype);\n            case card, deck ->\n                TtrpgConfig.getDefaultOutputSource(deck);\n            case legendaryGroup, monster, monsterfeatures ->\n                TtrpgConfig.getDefaultOutputSource(monster);\n            case item, itemGroup, magicvariant ->\n                TtrpgConfig.getDefaultOutputSource(item);\n            case object ->\n                TtrpgConfig.getDefaultOutputSource(object);\n            case race, subrace ->\n                TtrpgConfig.getDefaultOutputSource(race);\n            case table, tableGroup ->\n                TtrpgConfig.getDefaultOutputSource(table);\n            case trap, hazard ->\n                TtrpgConfig.getDefaultOutputSource(trap);\n            case vehicle, vehicleUpgrade ->\n                TtrpgConfig.getDefaultOutputSource(vehicle);\n            default -> TtrpgConfig.getDefaultOutputSource(this);\n        };\n    }\n\n    boolean hasVariants() {\n        return switch (this) {\n            case magicvariant, monster -> true;\n            default -> false;\n        };\n    }\n\n    boolean isFluffType() {\n        return switch (this) {\n            case backgroundFluff,\n                    classFluff,\n                    conditionFluff,\n                    facilityFluff,\n                    featFluff,\n                    hazardFluff,\n                    itemFluff,\n                    languageFluff,\n                    monsterFluff,\n                    objectFluff,\n                    optionalfeatureFluff,\n                    raceFluff,\n                    rewardFluff,\n                    subclassFluff,\n                    trapFluff,\n                    vehicleFluff ->\n                true;\n            default -> false;\n        };\n    }\n\n    boolean isDependentType() {\n        // These types are not directly filtered.\n        // Special rules are applied after the parent item is filtered\n        return switch (this) {\n            case card,\n                    classfeature,\n                    optionalFeatureTypes,\n                    subclass,\n                    subclassFeature ->\n                true;\n            default -> false;\n        };\n    }\n\n    boolean isOutputType() {\n        return useQuteNote() || writeFile();\n    }\n\n    enum IndexFields implements JsonNodeReader {\n        abbreviation,\n        appliesTo,\n        className,\n        classSource,\n        deck,\n        featureType,\n        level,\n        pantheon,\n        raceName,\n        raceSource,\n        set,\n        subclassShortName,\n        subclassSource,\n    }\n\n    public void withArrayFrom(JsonNode node, BiConsumer<Tools5eIndexType, JsonNode> callback) {\n        if (node.has(this.nodeName())) {\n            node.withArray(this.nodeName()).forEach(x -> callback.accept(this, x));\n        }\n    }\n\n    public void withArrayFrom(JsonNode node, String field, BiConsumer<Tools5eIndexType, JsonNode> callback) {\n        if (node.has(field)) {\n            node.withArray(field).forEach(x -> callback.accept(this, x));\n        }\n    }\n\n    boolean isKey(String crossRef) {\n        return crossRef != null && crossRef.startsWith(name());\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eJsonSourceCopier.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static dev.ebullient.convert.StringUtil.toTitleCase;\n\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.function.ToDoubleFunction;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.ArrayNode;\nimport com.fasterxml.jackson.databind.node.BooleanNode;\nimport com.fasterxml.jackson.databind.node.DoubleNode;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport com.fasterxml.jackson.databind.node.TextNode;\n\nimport dev.ebullient.convert.io.Msg;\nimport dev.ebullient.convert.tools.JsonCopyException;\nimport dev.ebullient.convert.tools.JsonSourceCopier;\nimport dev.ebullient.convert.tools.dnd5e.Json2QuteMonster.MonsterFields;\nimport dev.ebullient.convert.tools.dnd5e.Json2QuteRace.RaceFields;\n\npublic class Tools5eJsonSourceCopier extends JsonSourceCopier<Tools5eIndexType> implements JsonSource {\n    static final List<String> GENERIC_WALKER_ENTRIES_KEY_BLOCKLIST = List.of(\"caption\", \"type\", \"colLabels\", \"colLabelGroups\",\n            \"name\", \"colStyles\", \"style\", \"shortName\", \"subclassShortName\", \"id\", \"path\");\n\n    private static final List<String> _MERGE_REQUIRES_PRESERVE_BASE = List.of(\n            \"page\",\n            \"otherSources\",\n            \"srd\",\n            \"srd52\",\n            \"basicRules\",\n            \"basicRules2024\",\n            \"reprintedAs\",\n            \"hasFluff\",\n            \"hasFluffImages\",\n            \"hasToken\",\n            \"_versions\");\n    private static final Map<Tools5eIndexType, List<String>> _MERGE_REQUIRES_PRESERVE = Map.of(\n            // Monster fields that must be preserved\n            Tools5eIndexType.monster, List.of(\"legendaryGroup\", \"environment\", \"soundClip\",\n                    \"altArt\", \"variant\", \"dragonCastingColor\", \"familiar\"),\n            // Item fields that must be preserved\n            Tools5eIndexType.item, List.of(\"lootTables\", \"tier\"),\n            // Item Group fields that must be preserved\n            Tools5eIndexType.itemGroup, List.of(\"lootTables\", \"tier\"));\n    private static final List<String> COPY_ENTRY_PROPS = List.of(\n            \"action\", \"bonus\", \"reaction\", \"trait\", \"legendary\", \"mythic\", \"variant\", \"spellcasting\",\n            \"actionHeader\", \"bonusHeader\", \"reactionHeader\", \"legendaryHeader\", \"mythicHeader\");\n    static final List<String> LEARNED_SPELL_TYPE = List.of(\"constant\", \"will\", \"ritual\");\n    static final List<String> SPELL_CAST_FREQUENCY = List.of(\"recharge\", \"charges\", \"rest\", \"daily\", \"weekly\", \"yearly\");\n\n    static final Pattern dmg_avg_subst = Pattern.compile(\"([\\\\d.,]+)([+*-])([^$]+)\");\n\n    final Tools5eIndex index;\n\n    Tools5eJsonSourceCopier(Tools5eIndex index) {\n        this.index = index;\n    }\n\n    @Override\n    public Tools5eIndex index() {\n        return index;\n    }\n\n    @Override\n    public Tools5eSources getSources() {\n        throw new IllegalStateException(\"Should not call getSources while copying source\");\n    }\n\n    @Override\n    protected JsonNode getOriginNode(String key) {\n        return index().getOriginNoFallback(key);\n    }\n\n    @Override\n    protected boolean mergePreserveKey(Tools5eIndexType type, String key) {\n        List<String> preserveType = _MERGE_REQUIRES_PRESERVE.getOrDefault(type, List.of());\n        return _MERGE_REQUIRES_PRESERVE_BASE.contains(key) || preserveType.contains(key);\n    }\n\n    @Override\n    protected List<String> getCopyEntryProps() {\n        return COPY_ENTRY_PROPS;\n    }\n\n    // render.js: _getMergedSubrace\n    public JsonNode mergeSubrace(JsonNode subraceNode, JsonNode raceNode) {\n        ObjectNode copyFrom = (ObjectNode) copyNode(subraceNode);\n        ObjectNode subraceOut = (ObjectNode) copyNode(raceNode);\n\n        List.of(\"name\", \"source\", \"srd\", \"srd52\", \"basicRules\", \"basicRules2024\")\n                .forEach(p -> subraceOut.set(\"_base\" + toTitleCase(p), subraceOut.get(p)));\n        List.of(\"subraces\", \"srd\", \"srd52\", \"basicRules\", \"basicRules2024\",\n                \"_versions\", \"hasFluff\", \"hasFluffImages\",\n                \"reprintedAs\", \"_rawName\")\n                .forEach(subraceOut::remove);\n\n        copyFrom.remove(\"__prop\"); // cleanup\n\n        JsonNode overwrite = MetaFields.overwrite.getFrom(copyFrom);\n\n        // merge names\n        if (SourceField.name.existsIn(copyFrom)) {\n            String raceName = SourceField.name.getTextOrThrow(raceNode);\n            String subraceName = SourceField.name.getTextOrThrow(copyFrom);\n            subraceOut.put(\"_subraceName\", subraceName);\n            if (MetaFields.alias.existsIn(copyFrom)) {\n                ArrayNode alias = (ArrayNode) MetaFields.alias.getFrom(copyFrom);\n                for (int i = 0; i < alias.size(); i++) {\n                    alias.set(i, Json2QuteRace.getSubraceName(raceName, alias.get(i).asText()));\n                }\n                MetaFields.alias.setIn(subraceOut, alias);\n                MetaFields.alias.removeFrom(copyFrom);\n            }\n            SourceField.name.removeFrom(copyFrom);\n            SourceField.name.setIn(subraceOut, Json2QuteRace.getSubraceName(raceName, subraceName));\n        } else {\n            Tools5eFields.srd.copy(raceNode, subraceOut);\n            Tools5eFields.basicRules.copy(raceNode, subraceOut);\n        }\n\n        // merge abilities\n        if (RaceFields.ability.existsIn(copyFrom)) {\n            ArrayNode cpySrAbility = (ArrayNode) RaceFields.ability.getFrom(copyFrom);\n            ArrayNode outAbility = (ArrayNode) RaceFields.ability.getFrom(subraceOut);\n            // If the base race doesn't have any ability scores, make a set of empty records\n            if (RaceFields.ability.existsIn(overwrite) || outAbility == null) {\n                RaceFields.ability.copy(copyFrom, subraceOut);\n            } else if (cpySrAbility.size() != outAbility.size()) {\n                // if (cpy.ability.length !== cpySr.ability.length) throw new Error(`Race and subrace ability array lengths did not match!`);\n                tui().errorf(\"Error (%s): Unable to merge abilities (different lengths). CopyTo: %s, CopyFrom: %s\", subraceOut,\n                        copyFrom);\n            } else {\n                // cpySr.ability.forEach((obj, i) => Object.assign(cpy.ability[i], obj));\n                for (int i = 0; i < cpySrAbility.size(); i++) {\n                    outAbility.set(i, cpySrAbility.get(i));\n                }\n            }\n            RaceFields.ability.removeFrom(copyFrom);\n        }\n\n        // merge entries\n        if (SourceField.entries.existsIn(copyFrom)) {\n            ArrayNode entries = ensureArray(SourceField.entries.getFrom(subraceOut));\n            SourceField.entries.setIn(subraceOut, entries); // make sure set as array\n            for (JsonNode entry : iterableEntries(copyFrom)) {\n                JsonNode data = MetaFields.data.getFrom(entry);\n                if (MetaFields.overwrite.existsIn(data)) {\n                    // overwrite\n                    int index = findIndexByName(\"subrace-merge:\" + SourceField.name.getTextOrThrow(subraceOut),\n                            entries, MetaFields.overwrite.getTextOrThrow(data));\n                    if (index >= 0) {\n                        entries.set(index, entry);\n                    } else {\n                        appendToArray(entries, entry);\n                    }\n                } else {\n                    appendToArray(entries, entry);\n                }\n            }\n\n            SourceField.entries.removeFrom(copyFrom);\n        }\n\n        // TODO: ATM, not tracking trait tags, languages, or skills separately from text\n        if (Tools5eFields.traitTags.existsIn(copyFrom)) {\n            Tools5eFields.traitTags.removeFrom(copyFrom);\n        }\n        if (RaceFields.languageProficiencies.existsIn(copyFrom)) {\n            RaceFields.languageProficiencies.removeFrom(copyFrom);\n        }\n        if (RaceFields.skillProficiencies.existsIn(copyFrom)) {\n            RaceFields.skillProficiencies.removeFrom(copyFrom);\n        }\n\n        // overwrite everything else\n        for (Entry<String, JsonNode> e : iterableFields(copyFrom)) {\n            // already from a copy, just .. move it on over.\n            subraceOut.set(e.getKey(), e.getValue());\n        }\n\n        // For any null'd out fields on the subrace, delete the field\n        Iterator<Entry<String, JsonNode>> fields = subraceOut.properties().iterator();\n        while (fields.hasNext()) {\n            Entry<String, JsonNode> e = fields.next();\n            if (e.getValue().isNull()) {\n                fields.remove();\n            }\n        }\n\n        RaceFields._isSubRace.setIn(subraceOut, BooleanNode.TRUE);\n        return subraceOut;\n    }\n\n    // \tutils.js: static getCopy (impl, copyFrom, copyTo, templateData,...) {\n    @Override\n    public JsonNode mergeNodes(Tools5eIndexType type, String originKey, JsonNode copyFrom, ObjectNode target) {\n        JsonNode _copy = MetaFields._copy.getFromOrEmptyObjectNode(target);\n        normalizeMods(_copy);\n\n        // fetch and apply any external template\n        // append them to existing copy mods where available\n        ArrayNode templates = MetaFields._templates.readArrayFrom(_copy);\n        for (JsonNode _template : templates) {\n\n            String templateKey = Tools5eIndexType.monsterTemplate.createKey(_template);\n            JsonNode templateNode = getOriginNode(templateKey);\n\n            if (templateNode == null) {\n                tui().warnf(Msg.NOT_SET.wrap(\"Unable to find traits for %s\"), templateKey);\n                continue;\n            } else {\n                if (!MetaFields._mod.nestedExistsIn(MetaFields.apply, templateNode)) {\n                    // if template.apply._mod doesn't exist, skip this\n                    continue;\n                }\n\n                JsonNode template = copyNode(templateNode); // copy fast\n                JsonNode templateApply = MetaFields.apply.getFrom(template);\n                normalizeMods(templateApply);\n\n                JsonNode templateApplyMods = MetaFields._mod.getFrom(templateApply);\n                if (MetaFields._mod.existsIn(_copy)) {\n                    ObjectNode copyMods = (ObjectNode) MetaFields._mod.getFrom(_copy);\n                    for (Entry<String, JsonNode> e : iterableFields(templateApplyMods)) {\n                        if (copyMods.has(e.getKey())) {\n                            appendToArray(copyMods.withArray(e.getKey()), e.getValue());\n                        } else {\n                            copyMods.set(e.getKey(), e.getValue());\n                        }\n                    }\n                } else {\n                    MetaFields._mod.setIn(_copy, templateApplyMods);\n                }\n            }\n        }\n        MetaFields._templates.removeFrom(_copy);\n\n        // Copy required values from...\n        copyValues(type, copyFrom, target, _copy);\n\n        // apply any root template properties after doing base copy\n        List<String> copyToRootProps = streamOfFieldNames(target).toList();\n        for (JsonNode template : templates) {\n            if (!MetaFields._root.nestedExistsIn(MetaFields.apply, template)) {\n                continue;\n            }\n            JsonNode templateApplyRoot = MetaFields._root.getFrom(MetaFields.apply.getFrom(template));\n            for (Entry<String, JsonNode> from : iterableFields(templateApplyRoot)) {\n                String k = from.getKey();\n                if (!copyToRootProps.contains(k)) {\n                    continue; // avoid overwriting any real properties with templates\n                }\n                target.set(k, copyNode(from.getValue()));\n            }\n        }\n\n        // Apply mods\n        applyMods(originKey, copyFrom, target, _copy);\n\n        // indicate that this is a copy, and remove copy metadata (avoid revisit)\n        cleanupCopy(target, copyFrom);\n\n        return target;\n    }\n\n    @Override\n    protected JsonNode resolveDynamicVariable(\n            String originKey, JsonNode value, JsonNode target, TemplateVariable variableMode, String[] params) {\n        return switch (variableMode) {\n            case name -> new TextNode(SourceField.name.getTextOrEmpty(target));\n            case short_name -> new TextNode(getShortName(target, false));\n            case title_short_name -> new TextNode(getShortName(target, true));\n            case dc, spell_dc -> {\n                if (params.length == 0 || !target.has(params[0])) {\n                    tui().errorf(\"Error (%s): Missing detail for %s\", originKey, value);\n                    yield value;\n                }\n                int mod = getAbilityModNumber(target.get(params[0]).asInt());\n                int pb = crToPb(MonsterFields.cr.getFrom(target));\n                yield new TextNode(\"\" + (8 + pb + mod));\n            }\n            case to_hit -> {\n                if (params.length == 0 || !target.has(params[0])) {\n                    tui().errorf(\"Error (%s): Missing detail for %s\", originKey, value);\n                    yield value;\n                }\n                int mod = getAbilityModNumber(target.get(params[0]).asInt());\n                int pb = crToPb(MonsterFields.cr.getFrom(target));\n                yield new TextNode(asModifier(pb + mod));\n            }\n            case damage_mod -> {\n                if (params.length == 0 || !target.has(params[0])) {\n                    tui().errorf(\"Error (%s): Missing detail for %s\", originKey, value);\n                    yield value;\n                }\n                int mod = getAbilityModNumber(target.get(params[0]).asInt());\n                yield new TextNode(mod == 0 ? \"\" : asModifier(mod));\n            }\n            case damage_avg -> {\n                Matcher m = dmg_avg_subst.matcher(params[0]);\n                if (m.matches()) {\n                    String amount = m.group(1);\n                    String op = m.group(2);\n                    int mod = getAbilityModNumber(target.get(m.group(3)).asInt());\n                    if (\"+\".equals(op)) {\n                        double total = Double.parseDouble(amount) + mod;\n                        yield new TextNode(\"\" + Math.floor(total));\n                    }\n                }\n                tui().errorf(\"Error (%s): Unrecognized damage average template %s\", originKey, value);\n                yield value;\n            }\n            default -> {\n                variableMode.notSupported(tui(), originKey, value);\n                yield value;\n            }\n        };\n    }\n\n    protected void doModProp(\n            String originKey, JsonNode modInfo, JsonNode copyFrom, String prop, ObjectNode target, ModFieldMode mode) {\n        if (mode == null) {\n            tui().errorf(\"Error (%s): Missing mode for modProp (add value to ModFieldMode): %s\", originKey, modInfo);\n            return;\n        }\n        switch (mode) {\n            // Bestiary\n            case addAllSaves, addAllSkills, addSaves -> mode.notSupported(tui(), originKey, modInfo);\n            case addSenses -> doAddSenses(originKey, modInfo, copyFrom, target); // no prop\n            case addSkills -> doAddSkills(originKey, modInfo, target); // no prop\n            case addSpells -> doAddSpells(originKey, modInfo, copyFrom, target); // no prop\n            case replaceSpells -> doReplaceSpells(originKey, modInfo, copyFrom, target); // no prop\n            case removeSpells -> doRemoveSpells(originKey, modInfo, copyFrom, target); // no prop\n            // MATH\n            case calculateProp -> mode.notSupported(tui(), originKey, modInfo);\n            case scalarMultXp -> doScalarMultXp(originKey, modInfo, target); // no prop\n            case scalarAddDc -> doScalarAddDc(originKey, modInfo, prop, target);\n            case scalarAddHit -> doScalarAddHit(originKey, modInfo, prop, target);\n            case maxSize -> doMaxSize(originKey, modInfo, target); // no prop\n            default -> super.doModProp(originKey, modInfo, copyFrom, prop, target, mode);\n        }\n    }\n\n    private void doScalarAddHit(String originKey, JsonNode modInfo, String prop, ObjectNode target) {\n        final Pattern hitPattern = Pattern.compile(\"\\\\{@hit ([-+]?\\\\d+)}\");\n        if (!target.has(prop)) {\n            return;\n        }\n\n        int scalar = MetaFields.scalar.getFrom(modInfo).asInt();\n        String fullNode = hitPattern.matcher(target.get(prop).toString())\n                .replaceAll((match) -> \"{@hit \" + (Integer.parseInt(match.group(1)) + scalar) + \"}\");\n        target.set(prop, createNode(fullNode));\n    }\n\n    private void doScalarAddDc(String originKey, JsonNode modInfo, String prop, ObjectNode target) {\n        final Pattern dcPattern = Pattern.compile(\"\\\\{@dc (\\\\d+)(?:\\\\|[^}]+)?}\");\n        if (!target.has(prop)) {\n            return;\n        }\n        int scalar = MetaFields.scalar.getFrom(modInfo).asInt();\n        String fullNode = dcPattern.matcher(target.get(prop).toString())\n                .replaceAll((match) -> \"{@dc \" + (Integer.parseInt(match.group(1)) + scalar) + \"}\");\n        target.set(prop, createNode(fullNode));\n    }\n\n    private void doScalarMultXp(String originKey, JsonNode modInfo, ObjectNode target) {\n        JsonNode crNode = Tools5eFields.cr.getFrom(target);\n        if (crNode == null) {\n            tui().errorf(\"Error (%s): modifying scalarMultXp on an object that does not define cr; %s\", originKey, target);\n            return;\n        }\n        double scalar = MetaFields.scalar.getFrom(modInfo).asDouble();\n        boolean floor = MetaFields.floor.booleanOrDefault(modInfo, false);\n\n        ToDoubleFunction<Double> scalarMult = (x) -> {\n            double value = x * scalar;\n            if (floor) {\n                value = Math.floor(value);\n            }\n            return value;\n        };\n\n        if (Tools5eFields.xp.existsIn(crNode)) {\n            double input = Tools5eFields.xp.getFrom(crNode).asDouble();\n            Tools5eFields.xp.setIn(target, new DoubleNode(scalarMult.applyAsDouble(input)));\n        } else {\n            double crValue = crToXp(crNode);\n            if (!Tools5eFields.cr.existsIn(crNode)) {\n                // Wrap cr in a containing object\n                ObjectNode crNodeRw = mapper().createObjectNode();\n                Tools5eFields.cr.setIn(crNodeRw, crNode);\n                Tools5eFields.cr.setIn(target, crNodeRw);\n                crNode = crNodeRw;\n            }\n            Tools5eFields.xp.setIn(crNode, new DoubleNode(scalarMult.applyAsDouble(crValue)));\n        }\n    }\n\n    private void doMaxSize(String originKey, JsonNode modInfo, ObjectNode target) {\n        final String SIZES = \"FDTSMLHGCV\";\n        if (!Tools5eFields.size.existsIn(target)) {\n            tui().errorf(\"Error (%s): enforcing maxSize on an object that does not define size; %s\", originKey, target);\n            return;\n        }\n\n        String maxValue = MetaFields.max.getTextOrEmpty(modInfo);\n        int maxIdx = SIZES.indexOf(maxValue);\n        if (maxValue.isBlank() || maxIdx < 0) {\n            tui().errorf(\"Error (%s): Invalid maxSize value %s\", originKey, maxValue.isBlank() ? \"(missing)\" : maxValue);\n            return;\n        }\n\n        ArrayNode size = Tools5eFields.size.ensureArrayIn(target);\n        List<JsonNode> collect = streamOf(size)\n                .filter(x -> SIZES.indexOf(x.asText()) <= maxIdx)\n                .collect(Collectors.toList());\n\n        if (size.size() != collect.size()) {\n            size.removeAll();\n            if (collect.isEmpty()) {\n                size.add(new TextNode(maxValue));\n            } else {\n                size.addAll(collect);\n            }\n        }\n    }\n\n    // static _doMod_addSpells ({copyTo, copyFrom,...\n    void doAddSpells(String originKey, JsonNode modInfo, JsonNode copyFrom, ObjectNode target) {\n        ObjectNode spellcasting = (ObjectNode) MonsterFields.spellcasting.getFirstFromArray(target);\n        if (spellcasting == null) {\n            tui().errorf(\"Error (%s): Can't add spells to a monster without spellcasting\", originKey);\n            throw new JsonCopyException(\"Can't add spells to a monster without spellcasting; copy/merge of \" + originKey);\n        }\n\n        if (MonsterFields.spells.existsIn(modInfo)) {\n            ObjectNode spells = ensureObjectNode(MonsterFields.spells.getFrom(spellcasting));\n            MonsterFields.spells.setIn(spellcasting, spells); // ensure saved as object\n\n            for (Entry<String, JsonNode> modSpellEntry : iterableFields(MonsterFields.spells.getFrom(modInfo))) {\n                String k = modSpellEntry.getKey();\n                if (!spells.has(k)) {\n                    spells.set(k, copyNode(modSpellEntry.getValue()));\n                    continue;\n                }\n\n                // merge the spell objects (yikes)\n                ObjectNode targetSpell = (ObjectNode) spells.get(k);\n                JsonNode modSpell = modSpellEntry.getValue();\n\n                for (Entry<String, JsonNode> modSpellProp : iterableFields(modSpell)) {\n                    String prop = modSpellProp.getKey();\n                    JsonNode modSpellList = modSpellProp.getValue();\n                    JsonNode tgtSpellList = targetSpell.get(prop);\n\n                    if (tgtSpellList == null) {\n                        targetSpell.set(prop, copyNode(modSpellList));\n                    } else if (tgtSpellList.isArray()) {\n                        // append and sort\n                        appendToArray((ArrayNode) tgtSpellList, copyNode(modSpellList));\n                        targetSpell.set(prop, sortArrayNode((ArrayNode) tgtSpellList));\n                    } else if (tgtSpellList.isObject()) {\n                        // throw. Not supported\n                        tui().errorf(\"Error (%s): Object at key %s, not an array\", originKey, prop);\n                        throw new JsonCopyException(\"Badly formed spell list in \" + originKey\n                                + \"; found JSON Object instead of an array for \" + prop);\n                    } else {\n                        // overwrite\n                        targetSpell.set(prop, copyNode(modSpellList));\n                    }\n                }\n            }\n        }\n\n        for (String type : LEARNED_SPELL_TYPE) {\n            if (modInfo.has(type)) {\n                ArrayNode spells = spellcasting.withArray(type);\n                spells.addAll((ArrayNode) modInfo.get(type));\n            }\n        }\n\n        for (String cast : SPELL_CAST_FREQUENCY) {\n            JsonNode modSpells = modInfo.get(cast);\n            if (modSpells == null) {\n                continue;\n            }\n            ObjectNode spells = ensureObjectNode(spellcasting.get(cast));\n            spellcasting.set(cast, spells); // ensure saved as object\n\n            for (int i = 1; i <= 9; i++) {\n                String k = i + \"\";\n                if (modSpells.has(k)) {\n                    ArrayNode spellList = spells.withArray(k);\n                    appendToArray(spellList, (ArrayNode) modSpells.get(k));\n                }\n\n                String each = i + \"e\";\n                if (modSpells.has(each)) {\n                    ArrayNode spellList = spells.withArray(each);\n                    appendToArray(spellList, (ArrayNode) modSpells.get(each));\n                }\n            }\n        }\n    }\n\n    void doReplaceSpells(String originKey, JsonNode modInfo, JsonNode copyFrom, ObjectNode target) {\n        ObjectNode spellcasting = (ObjectNode) MonsterFields.spellcasting.getFirstFromArray(target);\n        if (spellcasting == null) {\n            tui().errorf(\"Error (%s): Can't replace spells for a monster without spellcasting\", originKey);\n            throw new JsonCopyException(\n                    \"Can't replace spells for a monster without spellcasting; copy/merge of \" + originKey);\n        }\n\n        if (MonsterFields.spells.existsIn(modInfo) && MonsterFields.spells.existsIn(spellcasting)) {\n            ObjectNode spells = ensureObjectNode(MonsterFields.spells.getFrom(spellcasting));\n            MonsterFields.spells.setIn(spellcasting, spells); // ensure saved as object\n\n            for (Entry<String, JsonNode> modSpellEntry : iterableFields(MonsterFields.spells.getFrom(modInfo))) {\n                String k = modSpellEntry.getKey();\n                if (spells.has(k)) { // replace if exists\n                    JsonNode replaceMetas = modSpellEntry.getValue();\n                    ObjectNode currentSpells = (ObjectNode) spells.get(k);\n                    replaceSpells(originKey, currentSpells, replaceMetas, MonsterFields.spells.name());\n                }\n            }\n        }\n\n        JsonNode modDailySpells = MonsterFields.daily.getFrom(modInfo);\n        ObjectNode dailySpells = (ObjectNode) MonsterFields.daily.getFrom(spellcasting);\n        if (modDailySpells != null && dailySpells != null) {\n            for (int i = 1; i <= 9; i++) {\n                String k = i + \"\";\n                for (JsonNode replaceMetas : iterableElements(modDailySpells.get(k))) {\n                    replaceSpells(originKey, dailySpells, replaceMetas, k);\n                }\n                String each = i + \"e\";\n                for (JsonNode replaceMetas : iterableElements(modDailySpells.get(each))) {\n                    replaceSpells(originKey, dailySpells, replaceMetas, each);\n                }\n            }\n        }\n    }\n\n    void replaceSpells(String originKey, ObjectNode currentSpells, JsonNode replaceMetas, String k) {\n        replaceMetas = ensureArray(replaceMetas);\n        for (JsonNode replaceMeta : iterableElements(replaceMetas)) {\n            JsonNode with = ensureArray(MetaFields.with.getFrom(replaceMeta));\n            JsonNode replace = MetaFields.replace.getFrom(replaceMeta);\n            if (replace == null) {\n                tui().errorf(\"Error (%s): Missing replace value for %s\", originKey, replaceMeta);\n                continue;\n            }\n            ArrayNode spellList = currentSpells.withArray(k);\n\n            int index = findIndexByName(originKey, spellList, replace.asText());\n            if (index >= 0) {\n                spellList.remove(index);\n                insertIntoArray(spellList, index, with);\n            } else {\n                tui().errorf(\"Error (%s): Unable to find spell %s to replace\", originKey, replace);\n            }\n        }\n    }\n\n    // static _doMod_removeSpells ({copyTo, copyFrom,...\n    void doRemoveSpells(String originKey, JsonNode modInfo, JsonNode copyFrom, ObjectNode target) {\n        ObjectNode spellcasting = (ObjectNode) MonsterFields.spellcasting.getFirstFromArray(target);\n        if (spellcasting == null) {\n            tui().errorf(\"Error (%s): Can't remove spells from a monster without spellcasting\", originKey);\n            throw new JsonCopyException(\n                    \"Can't remove spells from a monster without spellcasting; copy/merge of \" + originKey);\n        }\n\n        if (MonsterFields.spells.existsIn(modInfo) && MonsterFields.spells.existsIn(spellcasting)) {\n            ObjectNode spells = (ObjectNode) MonsterFields.spells.getFrom(spellcasting);\n\n            for (Entry<String, JsonNode> modSpellEntry : iterableFields(MonsterFields.spells.getFrom(modInfo))) {\n                String k = modSpellEntry.getKey();\n                // Look for spell levels: spells.1.spells\n                if (MonsterFields.spells.existsIn(spells.get(k))) {\n                    removeSpells(originKey,\n                            MonsterFields.spells.ensureArrayIn(spells.get(k)),\n                            modSpellEntry.getValue());\n                }\n            }\n\n            for (String k : LEARNED_SPELL_TYPE) {\n                if (modInfo.has(k) && spellcasting.has(k)) {\n                    ArrayNode spellList = spellcasting.withArray(k);\n                    removeSpells(originKey, spellList, modInfo.get(k));\n                }\n            }\n\n            for (String cast : SPELL_CAST_FREQUENCY) {\n                if (modInfo.has(cast) && spellcasting.has(cast)) {\n                    ObjectNode spellCast = (ObjectNode) spellcasting.get(cast);\n                    for (int i = 1; i <= 9; i++) {\n                        String k = i + \"\";\n                        String each = i + \"e\";\n                        if (spellCast.has(k)) {\n                            ArrayNode interval = spellcasting.withArray(k);\n                            removeSpells(originKey, interval, modInfo.get(k));\n                        }\n                        if (spellCast.has(each)) {\n                            ArrayNode eachList = spellcasting.withArray(each);\n                            removeSpells(originKey, eachList, modInfo.get(each));\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    void removeSpells(String originKey, ArrayNode spellList, JsonNode removeSpellList) {\n        for (JsonNode spell : iterableElements(removeSpellList)) {\n            int index = findIndexByName(originKey, spellList, spell.asText());\n            if (index >= 0) {\n                spellList.remove(index);\n            }\n        }\n    }\n\n    void doAddSenses(String originKey, JsonNode modInfo, JsonNode copyFrom, ObjectNode target) {\n        ArrayNode senses = ensureArray(MonsterFields.senses.getFrom(target));\n        MonsterFields.senses.setIn(target, senses); // make sure set as array\n\n        JsonNode modSenses = ensureArray(MonsterFields.senses.getFrom(modInfo));\n        for (JsonNode modSense : iterableElements(modSenses)) {\n            boolean found = false;\n            String modType = MetaFields.type.getTextOrThrow(modSense);\n            int modRange = MetaFields.range.intOrThrow(modSense);\n            Pattern p = Pattern.compile(modType + \" (\\\\d+)\", Pattern.CASE_INSENSITIVE);\n            for (int i = 0; i < senses.size(); i++) {\n                Matcher m = p.matcher(senses.get(i).asText());\n                if (m.matches()) {\n                    found = true;\n                    int range = Integer.parseInt(m.group(1));\n                    if (range < modRange) {\n                        senses.set(i, new TextNode(modType + \" \" + modRange + \" ft.\"));\n                    }\n                    break;\n                }\n            }\n            if (!found) {\n                senses.add(new TextNode(modType + \" \" + modRange + \" ft.\"));\n            }\n        }\n    }\n\n    void doAddSkills(String originKey, JsonNode modInfo, ObjectNode target) {\n        ObjectNode allSkills = ensureObjectNode(MonsterFields.skill.getFrom(target));\n        MonsterFields.skill.setIn(target, allSkills); // ensure saved as object\n        int pb = crToPb(Tools5eFields.cr.getFrom(target));\n\n        JsonNode modSkills = ensureArray(MetaFields.skills.getFrom(modInfo));\n        for (Entry<String, JsonNode> entry : iterableFields(modSkills)) {\n            String modSkill = entry.getKey();\n            int abilityScore = intOrThrow(target, getAbilityForSkill(modSkill));\n            int abilityMod = getAbilityModNumber(abilityScore);\n\n            // mode: 1 = proficient; 2 = expert\n            int mode = MetaFields.mode.intOrThrow(entry.getValue());\n            int total = mode * pb + abilityMod;\n\n            if (allSkills.has(modSkill)) {\n                int existing = intOrThrow(allSkills, modSkill);\n                if (total > existing) {\n                    allSkills.put(modSkill, asModifier(total));\n                }\n            } else {\n                allSkills.put(modSkill, asModifier(total));\n            }\n        }\n    }\n\n    public static String getShortName(JsonNode target, boolean isTitleCase) {\n        String name = SourceField.name.getTextOrEmpty(target);\n        JsonNode shortName = Tools5eFields.shortName.getFrom(target);\n        boolean isNamedCreature = MonsterFields.isNamedCreature.booleanOrDefault(target, false);\n        String prefix = isNamedCreature\n                ? \"\"\n                : isTitleCase ? \"The \" : \"the \";\n\n        if (shortName != null) {\n            if (shortName.isBoolean() && shortName.asBoolean()) {\n                return prefix + name;\n            }\n            String text = shortName.asText();\n            String result = prefix;\n            if (prefix.isBlank() && isTitleCase) {\n                result += toTitleCase(text);\n            } else {\n                result += text.toLowerCase();\n            }\n            return result;\n        }\n\n        return prefix + getShortNameFromName(name, isNamedCreature);\n    }\n\n    public static String getShortNameFromName(String name, boolean isNamedCreature) {\n        String result = name.split(\",\")[0]\n                .replaceAll(\"(?i)(?:adult|ancient|young) \\\\w+ (dragon|dracolich)\", \"$1\");\n\n        return isNamedCreature\n                ? result.split(\" \")[0]\n                : result.toLowerCase();\n    }\n\n    int getAbilityModNumber(int abilityScore) {\n        return (int) Math.floor((abilityScore - 10) / 2);\n    }\n\n    String getAbilityForSkill(String skill) {\n        switch (skill) {\n            case \"athletics\":\n                return \"str\";\n            case \"acrobatics\":\n            case \"sleight of hand\":\n            case \"stealth\":\n                return \"dex\";\n            case \"arcana\":\n            case \"history\":\n            case \"investigation\":\n            case \"nature\":\n            case \"religion\":\n                return \"int\";\n            case \"animal handling\":\n            case \"insight\":\n            case \"medicine\":\n            case \"perception\":\n            case \"survival\":\n                return \"wis\";\n            case \"deception\":\n            case \"intimidation\":\n            case \"performance\":\n            case \"persuasion\":\n                return \"cha\";\n        }\n        throw new IllegalArgumentException(\"Unknown skill: \" + skill);\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eLinkifier.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static dev.ebullient.convert.StringUtil.toAnchorTag;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.config.TtrpgConfig;\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.tools.JsonTextConverter.SourceField;\nimport dev.ebullient.convert.tools.dnd5e.Json2QuteClass.SubclassKeyData;\nimport dev.ebullient.convert.tools.dnd5e.Json2QuteDeity.DeityField;\nimport dev.ebullient.convert.tools.dnd5e.Json2QuteMonster.MonsterType;\nimport dev.ebullient.convert.tools.dnd5e.JsonSource.Tools5eFields;\nimport dev.ebullient.convert.tools.dnd5e.OptionalFeatureIndex.OptionalFeatureType;\nimport dev.ebullient.convert.tools.dnd5e.Tools5eIndexType.IndexFields;\n\npublic class Tools5eLinkifier {\n    private static Tools5eLinkifier instance;\n\n    public static Tools5eLinkifier instance() {\n        if (instance == null) {\n            instance = new Tools5eLinkifier();\n        }\n        return instance;\n    }\n\n    Tools5eIndex index;\n    Tui tui;\n\n    private Tools5eLinkifier() {\n        reset();\n    }\n\n    public String monsterPath(boolean isNpc, String type) {\n        return getRelativePath(Tools5eIndexType.monster) + \"/\" + (isNpc ? \"npc\" : MonsterType.toDirectory(type));\n    }\n\n    public String monsterPath(boolean isNpc, MonsterType type) {\n        return getRelativePath(Tools5eIndexType.monster) + \"/\" + (isNpc ? \"npc\" : type.toDirectory());\n    }\n\n    public String vaultRoot(Tools5eSources sources) {\n        return vaultRoot(sources.getType());\n    }\n\n    public String vaultRoot(Tools5eIndexType type) {\n        return type.useCompendiumBase()\n                ? index.compendiumVaultRoot()\n                : index.rulesVaultRoot();\n    }\n\n    public String getRelativePath(Tools5eSources sources) {\n        return getRelativePath(sources.getType());\n    }\n\n    public String getRelativePath(Tools5eIndexType type) {\n        return switch (type) {\n            case adventureData -> \"adventures\";\n            case bookData -> \"books\";\n            case card, deck -> \"decks\";\n            case classtype, subclass -> \"classes\";\n            case condition, status -> \"conditions\";\n            case deity -> \"deities\";\n            case facility -> \"bastions\";\n            case item, itemGroup -> \"items\";\n            case itemType -> \"item-types\";\n            case itemMastery -> \"item-mastery\";\n            case itemProperty -> \"item-properties\";\n            case legendaryGroup -> \"bestiary/legendary-group\";\n            case magicvariant -> \"items\";\n            case monster -> \"bestiary\";\n            case optfeature -> \"optional-features\";\n            case optionalFeatureTypes, spellIndex -> \"lists\";\n            case race, subrace -> TtrpgConfig.getConfig().racesAsSpecies() ? \"species\" : \"races\";\n            case table, tableGroup -> \"tables\";\n            case trap, hazard -> \"traps-hazards\";\n            case variantrule -> \"variant-rules\";\n            default -> type.name() + 's';\n        };\n    }\n\n    public String getTargetFileName(String name, Tools5eSources sources) {\n        Tools5eIndexType type = sources.getType();\n        JsonNode node = sources.findNode();\n        return switch (type) {\n            case background -> fixFileName(decoratedName(type, node), sources.primarySource(), type);\n            case deity -> getDeityResourceName(sources);\n            case subclass -> getSubclassResource(sources.getKey());\n            default -> fixFileName(name, sources.primarySource(), type);\n        };\n    }\n\n    public String getTargetFileName(String name, String source, Tools5eIndexType type) {\n        return fixFileName(name, source, type);\n    }\n\n    private String fixFileName(String fileName, Tools5eSources sources) {\n        return fixFileName(fileName, sources.primarySource(), sources.getType());\n    }\n\n    private String fixFileName(String fileName, String primarySource, Tools5eIndexType type) {\n        if (type == Tools5eIndexType.adventureData\n                || type == Tools5eIndexType.adventure\n                || type == Tools5eIndexType.book\n                || type == Tools5eIndexType.bookData\n                || type == Tools5eIndexType.tableGroup) {\n            return Tui.slugify(fileName); // file name is based on chapter, etc.\n        }\n        return Tui.slugify(fileName.replaceAll(\" \\\\(\\\\*\\\\)\", \"-gv\")\n                + sourceIfNotDefault(primarySource, type));\n    }\n\n    private static String sourceIfNotDefault(String source, Tools5eIndexType type) {\n        if (type == null) {\n            return \"\";\n        }\n        String defaultSource = type.defaultOutputSource();\n        // Special cases for items that are in the phb or xphb\n        if (type == Tools5eIndexType.item\n                || type == Tools5eIndexType.itemGroup\n                || type == Tools5eIndexType.magicvariant) {\n            if (source.equalsIgnoreCase(\"phb\") && defaultSource.equalsIgnoreCase(\"dmg\")) {\n                return \"\";\n            }\n            if (source.equalsIgnoreCase(\"xphb\") && defaultSource.equalsIgnoreCase(\"xdmg\")) {\n                return \"\";\n            }\n        }\n        // Special cases for monsters that are in the phb or xphb\n        if (type == Tools5eIndexType.monster\n                || type == Tools5eIndexType.legendaryGroup) {\n            if (source.equalsIgnoreCase(\"phb\") && defaultSource.equalsIgnoreCase(\"mm\")) {\n                return \"\";\n            }\n            if (source.equalsIgnoreCase(\"xphb\") && defaultSource.equalsIgnoreCase(\"xmm\")) {\n                return \"\";\n            }\n        }\n        if (source.equalsIgnoreCase(defaultSource)) {\n            return \"\";\n        }\n\n        return \"-\" + Tui.slugify(source);\n    }\n\n    // --- create links ---\n\n    public String link(String linkText, String key) {\n        if (key == null || index.isExcluded(key)) {\n            return linkText;\n        }\n        Tools5eSources linkSource = Tools5eSources.findSources(key);\n        return createLink(linkText, key, linkSource);\n    }\n\n    public String link(Tools5eSources linkSource) {\n        JsonNode node = linkSource.findNode();\n        String linkText = decoratedName(node);\n        String key = linkSource.getKey();\n        if (index.isExcluded(key)) {\n            return linkText;\n        }\n        return createLink(linkText, key, linkSource);\n    }\n\n    public String link(String linkText, Tools5eSources linkSource) {\n        String key = linkSource.getKey();\n        if (index.isExcluded(key)) {\n            return linkText;\n        }\n        return createLink(linkText, key, linkSource);\n    }\n\n    private String createLink(String linkText, String key, Tools5eSources linkSource) {\n        Tools5eIndexType type = Tools5eIndexType.getTypeFromKey(key);\n        return switch (type) {\n            case action,\n                    condition,\n                    disease,\n                    sense,\n                    skill,\n                    status ->\n                linkRule(linkText, key);\n            case card -> linkCard(linkText, key);\n            case classtype -> linkClass(linkText, key);\n            case classfeature -> linkClassFeature(linkText, key);\n            case monster -> linkCreature(linkText, key);\n            case deity -> linkDeity(linkText, key);\n            case subclass -> linkSubclass(linkText, key);\n            case variantrule -> linkVariantRules(linkText, key);\n            case table, tableGroup -> {\n                if (index.isIncluded(key)) {\n                    index.recordTableReference(key);\n                }\n                JsonNode node = index.getNode(key);\n                yield linkOrText(linkText, key,\n                        getRelativePath(type),\n                        fixFileName(decoratedName(type, node), linkSource.primarySource(), type));\n            }\n            default -> {\n                JsonNode node = index.getNode(key);\n                yield linkOrText(linkText, key,\n                        getRelativePath(type),\n                        fixFileName(decoratedName(type, node), linkSource.primarySource(), type));\n            }\n        };\n    }\n\n    private String linkOrText(String linkText, String key, String dirName, String resourceName) {\n        return index.isIncluded(key)\n                ? \"[%s](%s%s/%s.md)\".formatted(linkText,\n                        index.compendiumVaultRoot(),\n                        dirName,\n                        slugify(resourceName))\n                : linkText;\n    }\n\n    private String linkCard(String linkText, String cardKey) {\n        Tools5eSources cardSources = Tools5eSources.findSources(cardKey);\n        JsonNode node = cardSources.findNode();\n        String cardName = cardSources.getName();\n        String deckName = IndexFields.set.getTextOrThrow(node).trim();\n\n        return \"[%s](%s%s/%s.md#%s)\".formatted(\n                linkText,\n                index.compendiumVaultRoot(),\n                getRelativePath(Tools5eIndexType.deck),\n                fixFileName(deckName, cardSources.primarySource(), Tools5eIndexType.card),\n                cardName.replace(\" \", \"%20\"));\n    }\n\n    private String linkClass(String linkText, String classKey) {\n        Tools5eSources classSources = Tools5eSources.findSources(classKey);\n        return linkOrText(linkText, classKey,\n                getRelativePath(Tools5eIndexType.classtype),\n                getClassResource(classSources.getName(), classSources.primarySource()));\n    }\n\n    private String linkClassFeature(String linkText, String featureKey) {\n        JsonNode featureNode = index.getNode(featureKey);\n        Tools5eSources featureSources = Tools5eSources.findSources(featureKey);\n        int level = IndexFields.level.intOrThrow(featureNode); // required\n\n        String headerName = decoratedFeatureTypeName(featureSources, featureNode) + \" (Level \" + level + \")\";\n        String resource = slugify(getClassResource(\n                IndexFields.className.getTextOrEmpty(featureNode),\n                IndexFields.classSource.getTextOrEmpty(featureNode)));\n\n        return \"[%s](%s%s/%s.md#%s)\".formatted(\n                linkText,\n                index.compendiumVaultRoot(),\n                getRelativePath(Tools5eIndexType.classtype),\n                resource,\n                toAnchorTag(headerName));\n    }\n\n    private String linkCreature(String linkText, String creatureKey) {\n        JsonNode node = index.getNode(creatureKey);\n        Tools5eSources sources = Tools5eSources.findSources(creatureKey);\n\n        MonsterType creatureType = MonsterType.fromNode(node, index); // may be missing for partial index\n        String resourceName = decoratedName(Tools5eIndexType.monster, node);\n        boolean isNpc = Json2QuteMonster.isNpc(node);\n\n        return linkOrText(linkText, creatureKey,\n                monsterPath(isNpc, creatureType),\n                fixFileName(resourceName, sources.primarySource(), Tools5eIndexType.monster));\n    }\n\n    private String linkDeity(String linkText, String deityKey) {\n        Tools5eSources deitySources = Tools5eSources.findSources(deityKey);\n        return linkOrText(linkText, deityKey,\n                getRelativePath(Tools5eIndexType.deity),\n                getDeityResourceName(deitySources));\n    }\n\n    public String linkOptionalFeature(String linkText, String featureType) {\n        OptionalFeatureType oft = index.getOptionalFeatureType(featureType);\n        if (oft == null) {\n            return linkText;\n        }\n        if (linkText.equals(featureType)) {\n            linkText = oft.getTitle();\n        }\n        return linkOrText(linkText, oft.getKey(),\n                getRelativePath(Tools5eIndexType.optionalFeatureTypes),\n                oft.getFilename());\n    }\n\n    private String linkRule(String linkText, String ruleKey) {\n        Tools5eSources sources = Tools5eSources.findSources(ruleKey);\n        String sectionName = sources == null ? linkText : sources.getName();\n        Tools5eIndexType type = Tools5eIndexType.getTypeFromKey(ruleKey);\n        String relativePath = getRelativePath(type);\n\n        if (TtrpgConfig.getConfig().splitRules()) {\n            // Folder note: conditions/conditions.md#Blinded\n            String folderNote = relativePath + \"/\" + relativePath;\n            return \"[%s](%s%s.md#%s)\".formatted(\n                    linkText,\n                    index.rulesVaultRoot(),\n                    folderNote,\n                    toAnchorTag(sectionName));\n        }\n\n        return \"[%s](%s%s.md#%s)\".formatted(\n                linkText,\n                index.rulesVaultRoot(),\n                relativePath,\n                toAnchorTag(sectionName));\n    }\n\n    public String linkSpellEntry(Tools5eSources sources) {\n        JsonNode spellNode = sources.findNode();\n        String name = decoratedName(Tools5eIndexType.spell, spellNode);\n        return \"[%s](%s%s/%s.md \\\"%s\\\")\".formatted(name,\n                Tools5eIndex.instance().compendiumVaultRoot(),\n                getRelativePath(Tools5eIndexType.spell),\n                fixFileName(name, sources),\n                sources.primarySource());\n    }\n\n    public String linkSubclass(String linkText, String subclassKey) {\n        return linkOrText(linkText, subclassKey,\n                getRelativePath(Tools5eIndexType.classtype),\n                getSubclassResource(subclassKey));\n    }\n\n    public String linkSubclassFeature(String linkText,\n            String featureKey, JsonNode featureJson,\n            String subclassKey, JsonNode subclassNode) {\n\n        if (index.isExcluded(featureKey)) {\n            return linkText;\n        }\n\n        Tools5eSources featureSources = Tools5eSources.findSources(featureKey);\n\n        String level = Tools5eFields.level.getTextOrEmpty(featureJson);\n        String headerName = decoratedFeatureTypeName(featureSources, featureJson) + \" (Level \" + level + \")\";\n        String resource = slugify(getSubclassResource(\n                SourceField.name.getTextOrEmpty(subclassNode),\n                IndexFields.className.getTextOrEmpty(subclassNode),\n                IndexFields.classSource.getTextOrEmpty(subclassNode),\n                SourceField.source.getTextOrEmpty(subclassNode)));\n        return \"[%s](%s%s/%s.md#%s)\".formatted(\n                linkText,\n                index.compendiumVaultRoot(),\n                getRelativePath(Tools5eIndexType.classtype),\n                resource,\n                toAnchorTag(headerName));\n    }\n\n    private String linkVariantRules(String linkText, String rulesKey) {\n        Tools5eSources rulesSources = Tools5eSources.findSources(rulesKey);\n        String name = rulesSources.getName();\n        return \"[%s](%s%s/%s.md)\".formatted(\n                linkText,\n                index.rulesVaultRoot(),\n                getRelativePath(Tools5eIndexType.variantrule),\n                fixFileName(name, rulesSources));\n    }\n\n    // --- construct resource names ---\n\n    public String getDeityResourceName(Tools5eSources deitySources) {\n        String name = deitySources.getName();\n        String source = deitySources.primarySource();\n        JsonNode node = deitySources.findNode();\n        String pantheon = DeityField.pantheon.getTextOrEmpty(node);\n        String suffix = \"\";\n        switch (pantheon.toLowerCase()) {\n            case \"exandria\" -> {\n                suffix = TtrpgConfig.getConfig().sourceIncluded(\"egw\") && source.equalsIgnoreCase(\"egw\")\n                        ? \"\"\n                        : (\"-\" + Tui.slugify(source));\n            }\n            case \"dragonlance\" -> {\n                suffix = TtrpgConfig.getConfig().sourceIncluded(\"dsotdq\") && source.equalsIgnoreCase(\"dsotdq\")\n                        ? \"\"\n                        : (\"-\" + Tui.slugify(source));\n            }\n            default -> {\n                suffix = sourceIfNotDefault(source, Tools5eIndexType.deity);\n            }\n        }\n        return Tui.slugify(pantheon + \"-\" + name) + suffix;\n    }\n\n    public String getClassResource(String className, String classSource) {\n        return fixFileName(className, classSource, Tools5eIndexType.classtype);\n    }\n\n    public String getSubclassResource(String subclassKey) {\n        SubclassKeyData subclassData = new SubclassKeyData(subclassKey);\n        return getSubclassResource(subclassData.name(),\n                subclassData.parentName(), subclassData.parentSource(),\n                subclassData.itemSource());\n    }\n\n    public String getSubclassResource(String subclass, String parentClass, String classSource, String subclassSource) {\n        // Resolve classSource through reprints (e.g., PHB → XPHB when PHB class is reprinted)\n        classSource = index.resolveClassSource(parentClass, classSource);\n        String parentFile = Tui.slugify(parentClass);\n        String defaultSource = Tools5eIndexType.classtype.defaultOutputSource();\n        if (!classSource.equalsIgnoreCase(defaultSource) &&\n                (classSource.equalsIgnoreCase(\"phb\") || classSource.equalsIgnoreCase(\"xphb\"))) {\n            // For the most part, all subclasses are derived from the basic classes.\n            // There wasn't really a need to include the class source in the file name.\n            // However, the XPHB has created duplicates of all of the base classes.\n            // So if the parent class is not from the default source, we need to include\n            // its source in the file name if it's from the PHB or XPHB.\n            parentFile += \"-\" + classSource;\n        }\n        return fixFileName(\n                parentFile + \"-\" + Tui.slugify(subclass),\n                subclassSource,\n                Tools5eIndexType.subclass);\n    }\n\n    public String getOptionalFeatureTypeResource(String name) {\n        return slugify(\"list-optfeaturetype-\" + name);\n    }\n\n    public String getClassSpellList(JsonNode classNode) {\n        return getClassSpellList(SourceField.name.getTextOrEmpty(classNode));\n    }\n\n    public String getClassSpellList(String className) {\n        return \"list-spells-%s-%s\".formatted(\n                getRelativePath(Tools5eIndexType.classtype),\n                className.toLowerCase());\n    }\n\n    public String getSpellList(String name, Tools5eSources sources) {\n        Tools5eIndexType type = sources.getType();\n        JsonNode node = sources.findNode();\n        if (type == Tools5eIndexType.classtype) {\n            return getClassSpellList(node);\n        }\n        final String fileResource = fixFileName(name, sources);\n        return \"list-spells-%s-%s\".formatted(getRelativePath(type), fileResource);\n    }\n\n    public String decoratedName(JsonNode entry) {\n        Tools5eIndexType type = Tools5eIndexType.getTypeFromNode(entry);\n        return decoratedName(type, entry);\n    }\n\n    public String decoratedName(Tools5eIndexType type, JsonNode entry) {\n        String name = SourceField.name.getTextOrEmpty(entry);\n        switch (type) {\n            case background -> {\n                if (name.startsWith(\"Variant\")) {\n                    name = name.replace(\"Variant \", \"\") + \" (Variant)\";\n                }\n            }\n            case race, subrace -> {\n                name = name.replace(\"Variant; \", \"\");\n            }\n            default -> {\n            }\n        }\n        return decoratedName(name, entry);\n    }\n\n    public String decoratedName(String name, JsonNode entry) {\n        Tools5eSources sources = Tools5eSources.findOrTemporary(entry);\n        if (Tools5eIndex.isSrdBasicOnly() && sources.isSrdOrBasicRules()) {\n            String srcName = SourceField.name.getTextOrEmpty(entry);\n            if (name.equalsIgnoreCase(srcName)) {\n                // SRD name may be different / generic\n                name = sources.getName();\n            }\n        }\n        return Tools5eIndex.instance().replaceText(name);\n    }\n\n    public String decoratedFeatureTypeName(Tools5eSources valueSources, JsonNode value) {\n        String name = valueSources.getName();\n        String type = IndexFields.featureType.getTextOrEmpty(value);\n\n        if (!type.isEmpty()) {\n            switch (type) {\n                case \"D\":\n                    return \"Dragon Mark: \" + name;\n                case \"ED\":\n                    return \"Elemental Discipline: \" + name;\n                case \"EI\":\n                    return \"Eldritch Invocation: \" + name;\n                case \"MM\":\n                    return \"Metamagic: \" + name;\n                case \"MV\":\n                case \"MV:B\":\n                case \"MV:C2-UA\":\n                    return \"Maneuver: \" + name;\n                case \"FS:F\":\n                case \"FS:B\":\n                case \"FS:R\":\n                case \"FS:P\":\n                    return \"Fighting Style: \" + name;\n                case \"AS\":\n                case \"AS:V1-UA\":\n                case \"AS:V2-UA\":\n                    return \"Arcane Shot: \" + name;\n                case \"PB\":\n                    return \"Pact Boon: \" + name;\n                case \"AI\":\n                    return \"Artificer Infusion: \" + name;\n                case \"SHP:H\":\n                case \"SHP:M\":\n                case \"SHP:W\":\n                case \"SHP:F\":\n                case \"SHP:O\":\n                    return \"Ship Upgrade: \" + name;\n                case \"IWM:W\":\n                    return \"Infernal War Machine Variant: \" + name;\n                case \"IWM:A\":\n                case \"IWM:G\":\n                    return \"Infernal War Machine Upgrade: \" + name;\n                case \"OR\":\n                    return \"Onomancy Resonant: \" + name;\n                case \"RN\":\n                    return \"Rune Knight Rune: \" + name;\n                case \"AF\":\n                    return \"Alchemical Formula: \" + name;\n                default:\n                    tui.errorf(\"Unknown feature type %s for class feature %s\", type, name);\n            }\n        }\n        return name;\n    }\n\n    String slugify(String s) {\n        return Tui.slugify(s);\n    }\n\n    void reset() {\n        index = Tools5eIndex.instance();\n        tui = Tui.instance();\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eMarkdownConverter.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.io.MarkdownWriter;\nimport dev.ebullient.convert.io.MarkdownWriter.IndexContext;\nimport dev.ebullient.convert.io.Msg;\nimport dev.ebullient.convert.qute.QuteBase;\nimport dev.ebullient.convert.qute.QuteNote;\nimport dev.ebullient.convert.tools.IndexType;\nimport dev.ebullient.convert.tools.MarkdownConverter;\nimport dev.ebullient.convert.tools.dnd5e.OptionalFeatureIndex.OptionalFeatureType;\nimport dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteNote;\n\npublic class Tools5eMarkdownConverter implements MarkdownConverter {\n    final Tools5eIndex index;\n    final MarkdownWriter writer;\n\n    public Tools5eMarkdownConverter(Tools5eIndex index, MarkdownWriter writer) {\n        this.index = index;\n        this.writer = writer;\n    }\n\n    public Tools5eMarkdownConverter writeAll() {\n        return writeFiles(List.of(Tools5eIndexType.values()));\n    }\n\n    public Tools5eMarkdownConverter writeImages() {\n        index.tui().verbosef(Msg.WRITING, \"Writing images and fonts\");\n        index.tui().copyImages(Tools5eSources.getImages());\n        index.tui().copyFonts(Tools5eSources.getFonts());\n        return this;\n    }\n\n    public Tools5eMarkdownConverter writeFiles(IndexType type) {\n        return writeFiles(List.of(type));\n    }\n\n    static class WritingQueue {\n        List<QuteBase> baseCompendium = new ArrayList<>();\n        List<QuteBase> baseRules = new ArrayList<>();\n        List<QuteNote> noteCompendium = new ArrayList<>();\n        List<QuteNote> noteRules = new ArrayList<>();\n\n        // Some state for combining notes\n        Map<Tools5eIndexType, Json2QuteCommon> combinedDocs = new HashMap<>();\n    }\n\n    public Tools5eMarkdownConverter writeFiles(List<? extends IndexType> types) {\n        if (index.notPrepared()) {\n            throw new IllegalStateException(\"Index must be prepared before writing files\");\n        }\n        if (types == null || types.isEmpty()) {\n            return this;\n        }\n        index.tui().verbosef(\"Converting data: %s\", types);\n\n        WritingQueue queue = new WritingQueue();\n\n        boolean hasTables = types.stream()\n                .anyMatch(t -> t == Tools5eIndexType.table || t == Tools5eIndexType.tableGroup);\n        boolean hasNonTables = types.stream()\n                .anyMatch(t -> t != Tools5eIndexType.table && t != Tools5eIndexType.tableGroup);\n\n        if (hasTables && hasNonTables && index.cfg().onlyReferencedTables()) {\n            // Non-tables first: renders content and populates referencedTableKeys\n            _writeFiles(types.stream()\n                    .filter(t -> t != Tools5eIndexType.table && t != Tools5eIndexType.tableGroup)\n                    .toList(), queue, false);\n            // Tables second: only write those actually linked from included content\n            _writeFiles(types.stream()\n                    .filter(t -> t == Tools5eIndexType.table || t == Tools5eIndexType.tableGroup)\n                    .toList(), queue, true);\n        } else {\n            // Config not set, only tables, or only non-tables — single pass, no filtering\n            _writeFiles(types, queue, false);\n        }\n\n        IndexContext ctx = new IndexContext(name -> switch (name) {\n            case \"npc\" -> \"NPC\";\n            case \"traps-hazards\" -> \"Traps & Hazards\";\n            default -> MarkdownWriter.toTitle(name);\n        }, path -> {\n            if (path.toString().contains(\"books\") || path.toString().contains(\"adventures\")) {\n                return MarkdownWriter.sortEntryByPath;\n            }\n            return MarkdownWriter.sortEntryByTitle;\n        });\n\n        writer.writeFiles(index.compendiumFilePath(), queue.baseCompendium, ctx);\n        writer.writeFiles(index.rulesFilePath(), queue.baseRules, ctx);\n\n        for (Json2QuteCommon value : queue.combinedDocs.values()) {\n            append(value.type, value.buildNote(), queue.noteCompendium, queue.noteRules);\n            if (value instanceof Json2QuteCompose compose) {\n                queue.noteRules.addAll(compose.getSplitNotes());\n            }\n        }\n\n        if (types.contains(Tools5eIndexType.spell) || types.contains(Tools5eIndexType.spellIndex)) {\n            // We're doing this one a different way:\n            // Too many different variations of spell list\n            var spellIndexParent = new Json2QuteSpellIndex(index);\n            queue.noteCompendium.addAll(spellIndexParent.buildNotes());\n        }\n\n        if (!Json2QuteBackground.traits.isEmpty()) {\n            queue.noteCompendium.addAll(new BackgroundTraits2Note(index).buildNotes());\n        }\n\n        writer.writeNotes(index.compendiumFilePath(), queue.noteCompendium, true, ctx);\n        writer.writeNotes(index.rulesFilePath(), queue.noteRules, false, ctx);\n\n        writer.writeIndexes(ctx);\n\n        return this;\n    }\n\n    private void _writeFiles(List<? extends IndexType> types, WritingQueue queue, boolean filterTables) {\n        for (var entry : index.includedEntries()) {\n            final String key = entry.getKey();\n            final JsonNode jsonSource = entry.getValue();\n\n            Tools5eIndexType nodeType = Tools5eIndexType.getTypeFromKey(key);\n            if (types.contains(Tools5eIndexType.race) && nodeType == Tools5eIndexType.subrace) {\n                // include subrace with race\n            } else if (!types.contains(nodeType)) {\n                continue;\n            }\n\n            if (nodeType.writeFile()) {\n                writeQuteBaseFiles(nodeType, key, jsonSource, queue);\n            } else if (nodeType.isOutputType() && nodeType.useQuteNote()) {\n                if (filterTables && (nodeType == Tools5eIndexType.table || nodeType == Tools5eIndexType.tableGroup)) {\n                    Tools5eSources sources = Tools5eSources.findSources(key);\n                    // Write if linked from rendered content, or explicitly targeted by a filter rule\n                    boolean explicitlyIncluded = sources != null\n                            && sources.filterRuleApplied()\n                            && sources.includedByConfig();\n                    if (index.isTableReferenced(key) || explicitlyIncluded) {\n                        writeQuteNoteFiles(nodeType, key, jsonSource, queue);\n                    } else {\n                        index.tui().logf(Msg.FILTER, \"(drop | unreferenced) %s\", key);\n                    }\n                } else {\n                    writeQuteNoteFiles(nodeType, key, jsonSource, queue);\n                }\n            }\n        }\n    }\n\n    private void writeQuteBaseFiles(Tools5eIndexType type, String key, JsonNode jsonSource, WritingQueue queue) {\n        var compendium = queue.baseCompendium;\n        var rules = queue.baseRules;\n        if (type == Tools5eIndexType.classtype) {\n            Json2QuteClass jsonClass = new Json2QuteClass(index, type, jsonSource);\n            QuteBase converted = jsonClass.build();\n            if (converted != null) {\n                compendium.add(converted);\n                compendium.addAll(jsonClass.buildSubclasses());\n            }\n        } else {\n            QuteBase converted = switch (type) {\n                case background -> new Json2QuteBackground(index, type, jsonSource).build();\n                case deck -> new Json2QuteDeck(index, type, jsonSource).build();\n                case deity -> new Json2QuteDeity(index, type, jsonSource).build();\n                case facility -> new Json2QuteBastion(index, type, jsonSource).build();\n                case feat -> new Json2QuteFeat(index, type, jsonSource).build();\n                case hazard, trap -> new Json2QuteHazard(index, type, jsonSource).build();\n                case item, itemGroup -> new Json2QuteItem(index, type, jsonSource).build();\n                case monster -> new Json2QuteMonster(index, type, jsonSource).build();\n                case object -> new Json2QuteObject(index, type, jsonSource).build();\n                case optfeature -> new Json2QuteOptionalFeature(index, type, jsonSource).build();\n                case psionic -> new Json2QutePsionicTalent(index, type, jsonSource).build();\n                case race, subrace -> new Json2QuteRace(index, type, jsonSource).build();\n                case reward -> new Json2QuteReward(index, type, jsonSource).build();\n                case spell -> new Json2QuteSpell(index, type, jsonSource).build();\n                case vehicle -> new Json2QuteVehicle(index, type, jsonSource).build();\n                default -> throw new IllegalArgumentException(\"Unsupported type \" + type);\n            };\n            if (converted != null) {\n                append(type, converted, compendium, rules);\n            }\n        }\n    }\n\n    private void writeQuteNoteFiles(Tools5eIndexType nodeType, String key, JsonNode node, WritingQueue queue) {\n        var compendiumDocs = queue.noteCompendium;\n        var ruleDocs = queue.noteRules;\n        var combinedDocs = queue.combinedDocs;\n        final var vrDir = linkifier().getRelativePath(Tools5eIndexType.variantrule);\n\n        switch (nodeType) {\n            case action -> {\n                Json2QuteCompose action = (Json2QuteCompose) combinedDocs.computeIfAbsent(nodeType,\n                        t -> new Json2QuteCompose(nodeType, index, \"Actions\"));\n                action.add(node);\n            }\n            case adventureData, bookData -> {\n                String metadataKey = key.replace(\"data|\", \"|\");\n                JsonNode metadata = index.getOrigin(metadataKey);\n                if (!node.has(\"data\")) {\n                    index.tui().errorf(\"No data for %s\", key);\n                } else if (metadata == null) {\n                    index.tui().errorf(\"Unable to find metadata (%s) for %s\", metadataKey, key);\n                } else if (index.isIncluded(metadataKey)) {\n                    compendiumDocs.addAll(new Json2QuteBook(index, nodeType, metadata, node).buildBook());\n                } else {\n                    index.tui().debugf(Msg.FILTER, \"%s is excluded\", metadataKey);\n                }\n            }\n            case status, condition -> {\n                Json2QuteCompose conditions = (Json2QuteCompose) combinedDocs.computeIfAbsent(\n                        Tools5eIndexType.condition,\n                        t -> new Json2QuteCompose(nodeType, index, \"Conditions\"));\n                conditions.add(node);\n            }\n            case disease -> {\n                Json2QuteCompose disease = (Json2QuteCompose) combinedDocs.computeIfAbsent(nodeType,\n                        t -> new Json2QuteCompose(nodeType, index, \"Diseases\"));\n                disease.add(node);\n            }\n            case itemMastery -> {\n                Json2QuteCompose itemMastery = (Json2QuteCompose) combinedDocs.computeIfAbsent(nodeType,\n                        t -> new Json2QuteCompose(nodeType, index, \"Item Mastery\"));\n                itemMastery.add(node);\n            }\n            case itemProperty -> {\n                Json2QuteCompose itemProperty = (Json2QuteCompose) combinedDocs.computeIfAbsent(nodeType,\n                        t -> new Json2QuteCompose(nodeType, index, \"Item Properties\"));\n                itemProperty.add(node);\n            }\n            case itemType -> {\n                Json2QuteCompose itemTypes = (Json2QuteCompose) combinedDocs.computeIfAbsent(nodeType,\n                        t -> new Json2QuteCompose(nodeType, index, \"Item Types\"));\n                itemTypes.add(node);\n            }\n            case legendaryGroup -> {\n                QuteNote converted = new Json2QuteLegendaryGroup(index, nodeType, node).buildNote();\n                if (converted != null) {\n                    compendiumDocs.add(converted);\n                }\n            }\n            case optionalFeatureTypes -> {\n                OptionalFeatureType oft = index.getOptionalFeatureType(node);\n                if (oft == null) {\n                    return;\n                }\n                QuteNote converted = new Json2QuteOptionalFeatureType(index, node, oft).buildNote();\n                if (converted != null) {\n                    compendiumDocs.add(converted);\n                }\n            }\n            case sense -> {\n                Json2QuteCompose sense = (Json2QuteCompose) combinedDocs.computeIfAbsent(nodeType,\n                        t -> new Json2QuteCompose(nodeType, index, \"Senses\"));\n                sense.add(node);\n            }\n            case skill -> {\n                Json2QuteCompose skill = (Json2QuteCompose) combinedDocs.computeIfAbsent(nodeType,\n                        t -> new Json2QuteCompose(nodeType, index, \"Skills\"));\n                skill.add(node);\n            }\n            case table, tableGroup -> {\n                Tools5eQuteNote tableNote = new Json2QuteTable(index, nodeType, node).buildNote();\n                if (tableNote.getName().equals(\"Damage Types\")) {\n                    ruleDocs.add(tableNote);\n                } else {\n                    compendiumDocs.add(tableNote);\n                }\n            }\n            case variantrule -> append(nodeType,\n                    new Json2QuteNote(index, nodeType, node)\n                            .useSuffix(true)\n                            .withImagePath(vrDir)\n                            .buildNote()\n                            .withTargetPath(vrDir),\n                    compendiumDocs, ruleDocs);\n            default -> {\n                // skip it\n            }\n        }\n    }\n\n    <T extends QuteBase> void append(Tools5eIndexType type, T note, List<T> compendium, List<T> rules) {\n        if (note != null) {\n            if (type.useCompendiumBase()) {\n                compendium.add(note);\n            } else {\n                rules.add(note);\n            }\n        }\n    }\n\n    private static Tools5eLinkifier linkifier() {\n        return Tools5eLinkifier.instance();\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/Tools5eSources.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static dev.ebullient.convert.StringUtil.isPresent;\n\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.Optional;\nimport java.util.Set;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.config.CompendiumConfig;\nimport dev.ebullient.convert.config.TtrpgConfig;\nimport dev.ebullient.convert.io.FontRef;\nimport dev.ebullient.convert.io.Msg;\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.qute.QuteBase;\nimport dev.ebullient.convert.tools.CompendiumSources;\nimport dev.ebullient.convert.tools.IndexType;\nimport dev.ebullient.convert.tools.JsonNodeReader;\nimport dev.ebullient.convert.tools.JsonTextConverter.SourceField;\nimport dev.ebullient.convert.tools.ToolsIndex.TtrpgValue;\nimport dev.ebullient.convert.tools.dnd5e.JsonSource.JsonMediaHref;\nimport dev.ebullient.convert.tools.dnd5e.JsonSource.TableFields;\nimport io.quarkus.qute.TemplateData;\n\n@TemplateData\npublic class Tools5eSources extends CompendiumSources {\n\n    private static final Map<String, Tools5eSources> keyToSources = new HashMap<>();\n    private static final Map<String, ImageRef> imageSourceToRef = new HashMap<>();\n    private static final Map<String, FontRef> fontSourceToRef = new HashMap<>();\n    private static final Map<String, List<QuteBase>> keyToInlineNotes = new HashMap<>();\n    private static final Set<String> basicRulesKeys = new HashSet<>();\n    private static final Set<String> basicRules2024Keys = new HashSet<>();\n\n    private static boolean isBasicRules(String key, JsonNode jsonElement) {\n        if (basicRulesKeys.isEmpty()) {\n            final JsonNode basicRules = TtrpgConfig.activeGlobalConfig(\"basicRules\");\n            basicRules.forEach(node -> basicRulesKeys.add(node.asText()));\n        }\n        return SourceAttributes.basicRules.coerceBooleanOrDefault(jsonElement, false)\n                || basicRulesKeys.contains(key);\n    }\n\n    private static boolean isBasicRules2024(String key, JsonNode jsonElement) {\n        if (basicRules2024Keys.isEmpty()) {\n            final JsonNode basicRules = TtrpgConfig.activeGlobalConfig(\"basicRules2024\");\n            basicRules.forEach(node -> basicRules2024Keys.add(node.asText()));\n        }\n        return SourceAttributes.basicRules2024.coerceBooleanOrDefault(jsonElement, false)\n                || basicRules2024Keys.contains(key);\n    }\n\n    public static boolean has2024basicSrd() {\n        // return true if any of the 2024 core sources are enabled\n        return List.of(\"srd52\", \"basicRules2024\")\n                .stream().anyMatch(TtrpgConfig.getConfig()::sourceIncluded);\n    }\n\n    public static boolean has2024Content() {\n        // return true if any of the 2024 core sources are enabled\n        return has2024basicSrd() || List.of(\"XPHB\", \"XDMG\", \"XMM\")\n                .stream().anyMatch(TtrpgConfig.getConfig()::sourceIncluded);\n    }\n\n    public static boolean has2014basicSrd() {\n        // return true if any of the 2024 core sources are enabled\n        return List.of(\"srd\", \"basicRules\")\n                .stream().anyMatch(TtrpgConfig.getConfig()::sourceIncluded);\n    }\n\n    public static boolean has2014Content() {\n        // return true if any of the 2024 core sources are enabled\n        return has2014basicSrd() || List.of(\"PHB\", \"DMG\", \"MM\")\n                .stream().anyMatch(TtrpgConfig.getConfig()::sourceIncluded);\n    }\n\n    public static boolean includedByConfig(String key) {\n        Tools5eSources sources = findSources(key);\n        return sources != null && sources.includedByConfig();\n    }\n\n    public static boolean excludedByConfig(String key) {\n        return !includedByConfig(key);\n    }\n\n    public static boolean filterRuleApplied(String key) {\n        Tools5eSources sources = findSources(key);\n        return sources != null && sources.filterRule;\n    }\n\n    public static Tools5eSources findSources(String key) {\n        if (key == null) {\n            return null;\n        }\n        return keyToSources.get(key);\n    }\n\n    public static Tools5eSources findSources(JsonNode node) {\n        String key = TtrpgValue.indexKey.getTextOrEmpty(node);\n        return keyToSources.get(key);\n    }\n\n    public static Tools5eSources constructSources(String key, JsonNode node) {\n        if (node == null) {\n            throw new IllegalArgumentException(\"Must pass a JsonNode\");\n        }\n        Tools5eIndexType type = Tools5eIndexType.getTypeFromKey(key);\n        TtrpgValue.indexKey.setIn(node, key);\n        return keyToSources.computeIfAbsent(key, k -> {\n            Tools5eSources s = new Tools5eSources(type, key, node);\n            s.checkKnown();\n            return s;\n        });\n    }\n\n    public static Tools5eSources findOrTemporary(JsonNode node) {\n        if (node == null) {\n            throw new IllegalArgumentException(\"Must pass a JsonNode\");\n        }\n        Tools5eIndexType type = Tools5eIndexType.getTypeFromNode(node);\n        if (type == null) {\n            type = SourceField.source.existsIn(node)\n                    ? Tools5eIndexType.reference\n                    : Tools5eIndexType.syntheticGroup;\n            TtrpgValue.indexInputType.setIn(node, type.name());\n        }\n        String key = TtrpgValue.indexKey.getTextOrNull(node);\n        if (key == null) {\n            key = type.createKey(node);\n            TtrpgValue.indexKey.setIn(node, key);\n        }\n        Tools5eSources sources = findSources(key);\n        return sources == null\n                ? new Tools5eSources(type, key, node)\n                : sources;\n    }\n\n    public static Collection<ImageRef> getImages() {\n        return imageSourceToRef.values();\n    }\n\n    public static Collection<QuteBase> getInlineNotes(String key) {\n        return keyToInlineNotes.getOrDefault(key, List.of());\n    }\n\n    public void addInlineNote(QuteBase note) {\n        keyToInlineNotes.computeIfAbsent(this.key, k -> new ArrayList<>()).add(note);\n    }\n\n    public static Collection<FontRef> getFonts() {\n        return fontSourceToRef.values().stream()\n                .filter(FontRef::hasTextReference)\n                .toList();\n    }\n\n    public static void addFonts(JsonNode source, JsonNodeReader field) {\n        if (field.isArrayIn(source)) {\n            for (JsonNode font : field.iterateArrayFrom(source)) {\n                addFont(font.asText());\n            }\n        } else if (field.isObjectIn(source)) {\n            for (Entry<String, JsonNode> font : field.iterateFieldsFrom(source)) {\n                addFont(font.getKey(), font.getValue().asText());\n            }\n        }\n    }\n\n    public static void addFont(String fontFamily, String fontString) {\n        FontRef ref = FontRef.of(fontFamily, fontString);\n        if (ref == null) {\n            Tui.instance().warnf(\"Font '%s' is invalid, empty, or not found\", fontString);\n        } else {\n            FontRef previous = fontSourceToRef.putIfAbsent(fontFamily, ref);\n            if (previous != null) {\n                Tui.instance().warnf(\"Font '%s' is already defined as '%s'\", fontString, previous);\n            }\n        }\n    }\n\n    public static void addFont(String fontString) {\n        String fontFamily = FontRef.fontFamily(fontString);\n        addFont(fontFamily, fontString);\n    }\n\n    public static String getFontReference(String fontString) {\n        String fontFamily = FontRef.fontFamily(fontString);\n        FontRef ref = fontSourceToRef.get(fontFamily);\n        if (ref == null) {\n            return null;\n        }\n        ref.addTextReference();\n        return fontFamily;\n    }\n\n    public static boolean isSrd(JsonNode node) {\n        return SourceAttributes.srd.coerceBooleanOrDefault(node, false)\n                || SourceAttributes.srd52.coerceBooleanOrDefault(node, false);\n    }\n\n    /** Return the srd name or null */\n    public static String srdName(JsonNode node) {\n        String name = SourceAttributes.srd52.getTextOrDefault(node, SourceAttributes.srd.getTextOrNull(node));\n        return \"true\".equalsIgnoreCase(name) ? null : name;\n    }\n\n    private final boolean srd;\n    private final boolean basicRules;\n    private final boolean srd52;\n    private final boolean basicRules2024;\n    private final boolean includedWhenNoSource;\n\n    private final Tools5eIndexType type;\n    private final String edition;\n\n    private boolean filterRule;\n    private boolean cfgIncluded;\n\n    private Tools5eSources(Tools5eIndexType type, String key, JsonNode jsonElement) {\n        super(type, key, jsonElement);\n        this.type = type;\n        this.basicRules = isBasicRules(key, jsonElement);\n        this.basicRules2024 = isBasicRules2024(key, jsonElement);\n        this.srd = SourceAttributes.srd.coerceBooleanOrDefault(jsonElement, false);\n        this.srd52 = SourceAttributes.srd52.coerceBooleanOrDefault(jsonElement, false);\n        this.includedWhenNoSource = this.srd52 || this.basicRules2024; // just 2024 when nothing specified\n\n        this.edition = SourceAttributes.edition.getTextOrEmpty(jsonElement);\n        addBrewSource(TtrpgValue.homebrewSource, jsonElement);\n        addBrewSource(TtrpgValue.homebrewBaseSource, jsonElement);\n        testSourceRules();\n    }\n\n    private void addBrewSource(JsonNodeReader field, JsonNode jsonElement) {\n        String source = field.getTextOrNull(jsonElement);\n        if (isPresent(source)) {\n            this.sources.add(source);\n        }\n    }\n\n    public boolean isHomebrew() {\n        JsonNode node = findNode();\n        return TtrpgValue.homebrewSource.existsIn(node) || TtrpgValue.homebrewBaseSource.existsIn(node);\n    }\n\n    public boolean isSrdOrBasicRules() {\n        return srd || basicRules || srd52 || basicRules2024;\n    }\n\n    /**\n     * Is this included by configuration (source list, include/exclude rules)?\n     * Content may be suppressed for other reasons (reprints)\n     */\n    public boolean includedByConfig() {\n        return cfgIncluded;\n    }\n\n    /**\n     * Was this targeted by an include/exclude rule?\n     */\n    public boolean filterRuleApplied() {\n        return filterRule;\n    }\n\n    public String getDecoratedName() {\n        return Tools5eLinkifier.instance().decoratedName(this.name, findNode());\n    }\n\n    private void testSourceRules() {\n        CompendiumConfig config = TtrpgConfig.getConfig();\n        Optional<Boolean> rulesSpecify = config.keyIsIncluded(key);\n        this.filterRule = rulesSpecify.isPresent();\n        this.cfgIncluded = testSourceRules(config, rulesSpecify);\n    }\n\n    /**\n     * Test if this source is included by the configuration\n     */\n    private boolean testSourceRules(CompendiumConfig config, Optional<Boolean> rulesSpecify) {\n        if (rulesSpecify.isPresent()) {\n            return rulesSpecify.get();\n        }\n        if (config.allSources()) {\n            return true;\n        }\n        if (config.noSources()) {\n            return this.includedWhenNoSource;\n        } else if (Tools5eIndex.isSrdBasicOnly()) {\n            return testSrdRules2014(config)\n                    || testSrdRules2024(config);\n        }\n        return testSourceIncluded(config)\n                || testSrdRules2014(config)\n                || testSrdRules2024(config);\n    }\n\n    private boolean testSourceIncluded(CompendiumConfig config) {\n        // backgrounds don't nest. Check only primary source\n        return type == Tools5eIndexType.background\n                ? config.sourceIncluded(this.primarySource())\n                : config.sourceIncluded(this);\n    }\n\n    private boolean testSrdRules2014(CompendiumConfig config) {\n        if (has2014Content()) {\n            return (config.sourceIncluded(\"srd\") && this.srd)\n                    || (config.sourceIncluded(\"basicrules\") && this.basicRules);\n        }\n        return false;\n    }\n\n    private boolean testSrdRules2024(CompendiumConfig config) {\n        if (has2024Content()) {\n            return (config.sourceIncluded(\"srd52\") && this.srd52)\n                    || (config.sourceIncluded(\"basicrules2024\") && this.basicRules2024);\n        }\n        return false;\n    }\n\n    @Override\n    public boolean includedBy(Set<String> sources) {\n        CompendiumConfig config = TtrpgConfig.getConfig();\n        if (config.noSources()) {\n            return this.includedWhenNoSource;\n        }\n        return super.includedBy(sources)\n                || (this.srd && sources.contains(\"srd\"))\n                || (this.srd52 && sources.contains(\"srd52\"))\n                || (this.basicRules && sources.contains(\"basicrules\"))\n                || (this.basicRules2024 && sources.contains(\"basicrules2024\"));\n    }\n\n    @Override\n    public Tools5eIndexType getType() {\n        return type;\n    }\n\n    @Override\n    protected boolean isSynthetic() {\n        return type == Tools5eIndexType.syntheticGroup;\n    }\n\n    public String edition() {\n        return edition;\n    }\n\n    public boolean isClassic() {\n        return \"classic\".equalsIgnoreCase(edition);\n    }\n\n    public String getSourceText() {\n        if (Tools5eIndex.isSrdBasicOnly()) {\n            List<String> bits = new ArrayList<>();\n            if (srd) {\n                bits.add(\"SRD 5.1\");\n            } else if (srd52) {\n                bits.add(\"SRD 5.2\");\n            }\n            if (basicRules2024) {\n                bits.add(\"the Free Rules (2024)\");\n            } else if (basicRules) {\n                bits.add(\"the Basic Rules (2014)\");\n            }\n            return String.join(\" and \", bits);\n        }\n        return super.getSourceText();\n    }\n\n    public JsonNode findNode() {\n        JsonNode result = Tools5eIndex.instance().getNode(key);\n        if (result == null) {\n            result = Tools5eIndex.instance().getOrigin(this.key);\n        }\n        return result;\n    }\n\n    protected String findName(IndexType type, JsonNode jsonElement) {\n        if (type == Tools5eIndexType.syntheticGroup) {\n            return this.key.replaceAll(\".+?\\\\|([^|]+).*\", \"$1\");\n        }\n\n        if (Tools5eIndex.isSrdBasicOnly() && Tools5eSources.isSrd(jsonElement)) {\n            String srdName = Tools5eSources.srdName(jsonElement);\n            if (srdName != null) {\n                if (this.key.contains(\"(13 cards)\") && !srdName.contains(\"13 cards\")) {\n                    // Special case for the 13-card deck\n                    srdName += \" (13 cards)\";\n                }\n                return srdName;\n            }\n        }\n\n        return SourceField.name.getTextOrDefault(jsonElement,\n                SourceField.abbreviation.getTextOrDefault(jsonElement,\n                        TableFields.caption.getTextOrDefault(jsonElement,\n                                \"unknown\")));\n    }\n\n    @Override\n    protected String findSourceText(IndexType type, JsonNode jsonElement) {\n        if (type == Tools5eIndexType.syntheticGroup) {\n            return this.key.replaceAll(\".*\\\\|([^|]+)\\\\|\", \"$1\");\n        }\n        if (type == Tools5eIndexType.reference) {\n            return \"\";\n        }\n        if (jsonElement == null) {\n            Tui.instance().logf(Msg.UNRESOLVED, \"Resource %s has no jsonElement\", this.key);\n            return \"\";\n        }\n        String srcText = super.findSourceText(type, jsonElement);\n\n        JsonNode basicRules = SourceAttributes.basicRules.getFrom(jsonElement);\n        JsonNode basicRules2024 = SourceAttributes.basicRules2024.getFrom(jsonElement);\n\n        JsonNode srd = SourceAttributes.srd.getFrom(jsonElement);\n        JsonNode srd52 = SourceAttributes.srd52.getFrom(jsonElement);\n\n        String srdText = \"\";\n        if (srd52 != null) {\n            srdText = \"the <span title='Systems Reference Document (5.2)'>SRD</span>\";\n            if (srd52.isTextual()) {\n                srdText += \" (as \\\"\" + Tools5eIndex.instance().replaceText(srd52.asText()) + \"\\\")\";\n            }\n        } else if (srd != null) {\n            srdText = \"the <span title='Systems Reference Document (5.1)'>SRD</span>\";\n            if (srd.isTextual()) {\n                srdText += \" (as \\\"\" + Tools5eIndex.instance().replaceText(srd.asText()) + \"\\\")\";\n            }\n        }\n\n        String basicRulesText = \"\";\n        if (basicRules2024 != null) {\n            basicRulesText = \"the Free Rules (2024)\";\n            if (basicRules2024.isTextual()) {\n                basicRulesText += \" (as \\\"\" + basicRules2024.asText() + \"\\\")\";\n            }\n        } else if (basicRules != null) {\n            basicRulesText = \"the Basic Rules (2014)\";\n            if (basicRules.isTextual()) {\n                basicRulesText += \" (as \\\"\" + basicRules.asText() + \"\\\")\";\n            }\n        }\n\n        String sourceText = String.join(\", \", srcText);\n        if (srdText.isBlank() && basicRulesText.isBlank()) {\n            return sourceText;\n        }\n        String srdBasic = \"Available in \" + srdText;\n        if (!srdText.isEmpty() && !basicRulesText.isEmpty()) {\n            srdBasic += \" and \";\n        }\n        srdBasic += basicRulesText;\n        return sourceText.isEmpty()\n                ? srdBasic\n                : sourceText + \". \" + srdBasic;\n    }\n\n    public Optional<String> uaSource() {\n        Optional<String> source = sources.stream().filter(x -> x.contains(\"UA\") && !x.equals(\"UAWGE\")).findFirst();\n        return source.map(TtrpgConfig::sourceToAbbreviation);\n    }\n\n    public ImageRef buildTokenImageRef(Tools5eIndex index, String sourcePath, Path target, boolean useCompendium) {\n        String key = sourcePath.toString();\n        ImageRef imageRef = new ImageRef.Builder()\n                .setRelativePath(target)\n                .setInternalPath(sourcePath)\n                .setRootFilepath(useCompendium ? index.compendiumFilePath() : index.rulesFilePath())\n                .setVaultRoot(useCompendium ? index.compendiumVaultRoot() : index.rulesVaultRoot())\n                .build(imageSourceToRef.get(key));\n        imageSourceToRef.putIfAbsent(key, imageRef);\n        return imageRef;\n    }\n\n    public ImageRef buildImageRef(Tools5eIndex index, JsonMediaHref mediaHref, String imageBasePath, boolean useCompendium) {\n        final String title = mediaHref.title == null ? \"\" : mediaHref.title;\n        final String altText = mediaHref.altText == null ? title : mediaHref.altText;\n        final String key = mediaHref.href.path == null\n                ? mediaHref.href.url\n                : mediaHref.href.path;\n\n        if (mediaHref.href.url == null && mediaHref.href.path == null) {\n            Tui.instance().errorf(\"We have an ImageRef (%s) with no path\", mediaHref);\n            ImageRef imageRef = new ImageRef.Builder()\n                    .setTitle(index.replaceText(altText))\n                    .build();\n            return imageRef;\n        }\n\n        String fullPath = mediaHref.href.path == null\n                ? mediaHref.href.url\n                : mediaHref.href.path.replace(\"\\\\\", \"/\");\n        int pos = fullPath.lastIndexOf('/');\n        // Replace %20 with space ahead of slugify if it is present\n        String fileName = fullPath.substring(pos + 1).replace(\"%20\", \" \");\n\n        int query = fileName.lastIndexOf('?');\n        if (query >= 0) {\n            fileName = fileName.substring(0, query);\n        }\n\n        if (type == Tools5eIndexType.deity || type == Tools5eIndexType.note || type == Tools5eIndexType.variantrule) {\n            fileName = primarySource() + \"-\" + fileName;\n        } else if (type == Tools5eIndexType.deck && !fullPath.contains(\"generic\")) {\n            int pos2 = fullPath.substring(0, pos).lastIndexOf('/');\n            fileName = fullPath.substring(pos2 + 1, pos) + \"-\" + fileName;\n        }\n\n        int x = fileName.lastIndexOf('.');\n        fileName = x < 0\n                ? index.slugify(fileName)\n                : index.slugify(fileName.substring(0, x)) + fileName.substring(x);\n        Path target = Path.of(imageBasePath, \"img\", fileName);\n\n        ImageRef.Builder builder = new ImageRef.Builder()\n                .setWidth(mediaHref.width)\n                .setTitle(index.replaceText(altText))\n                .setRelativePath(target)\n                .setRootFilepath(useCompendium ? index.compendiumFilePath() : index.rulesFilePath())\n                .setVaultRoot(useCompendium ? index.compendiumVaultRoot() : index.rulesVaultRoot());\n\n        if (mediaHref.href.path == null) {\n            builder.setUrl(mediaHref.href.url);\n        } else {\n            builder.setInternalPath(mediaHref.href.path);\n        }\n\n        ImageRef imageRef = builder.build(imageSourceToRef.get(key));\n        imageSourceToRef.putIfAbsent(key, imageRef);\n        return imageRef;\n    }\n\n    /** Amend optionalfeaturetype with sources of related optional features */\n    public void amendSources(Tools5eSources otherSources) {\n        this.sources.addAll(otherSources.sources);\n        this.bookRef.addAll(otherSources.bookRef);\n        testSourceRules();\n    }\n\n    public void amendSources(Set<String> brewSources) {\n        this.sources.addAll(brewSources);\n        testSourceRules();\n    }\n\n    public void amendHomebrewSources(JsonNode homebrewElement) {\n        addBrewSource(TtrpgValue.homebrewBaseSource, homebrewElement);\n        addBrewSource(TtrpgValue.homebrewSource, homebrewElement);\n        testSourceRules();\n    }\n\n    public boolean contains(Tools5eSources sources) {\n        Collection<String> sourcesList = sources.getSources();\n        return this.sources.stream().anyMatch(sourcesList::contains);\n    }\n\n    public enum SourceAttributes implements JsonNodeReader {\n        srd,\n        basicRules,\n        srd52,\n        basicRules2024,\n        edition;\n    }\n\n    public static void clear() {\n        keyToSources.clear();\n        imageSourceToRef.clear();\n        fontSourceToRef.clear();\n        keyToInlineNotes.clear();\n        basicRulesKeys.clear();\n        basicRules2024Keys.clear();\n    }\n\n    public static boolean isClassicEdition(JsonNode baseItem) {\n        String edition = SourceAttributes.edition.getTextOrDefault(baseItem, \"\");\n        return \"classic\".equalsIgnoreCase(edition);\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/qute/AbilityScores.java",
    "content": "package dev.ebullient.convert.tools.dnd5e.qute;\n\nimport static dev.ebullient.convert.StringUtil.asModifier;\n\nimport dev.ebullient.convert.qute.QuteUtil;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * 5eTools Ability Score attributes.\n *\n * Used to describe a monster, object or vehicle's ability scores.\n *\n * If referenced as a unit (ignoring inner attributes), it will render ability scores as\n * a `|` separated list of values, in `STR,DEX,CON,INT,WIS,CHA` order.\n *\n * For example:\n * `10 (+0)|10 (+0)|10 (+0)|10 (+0)|10 (+0)|10 (+0)`.\n *\n * @param strength Strength score as {@link dev.ebullient.convert.tools.dnd5e.qute.AbilityScores.AbilityScore}\n * @param dexterity Dexterity score as {@link dev.ebullient.convert.tools.dnd5e.qute.AbilityScores.AbilityScore}\n * @param constitution Constitution score as {@link dev.ebullient.convert.tools.dnd5e.qute.AbilityScores.AbilityScore}\n * @param intelligence Intelligence score as {@link dev.ebullient.convert.tools.dnd5e.qute.AbilityScores.AbilityScore}\n * @param wisdom Wisdom score as {@link dev.ebullient.convert.tools.dnd5e.qute.AbilityScores.AbilityScore}\n * @param charisma Charisma score as {@link dev.ebullient.convert.tools.dnd5e.qute.AbilityScores.AbilityScore}\n */\n@TemplateData\npublic record AbilityScores(\n        AbilityScore strength,\n        AbilityScore dexterity,\n        AbilityScore constitution,\n        AbilityScore intelligence,\n        AbilityScore wisdom,\n        AbilityScore charisma) implements QuteUtil {\n\n    public static final AbilityScore TEN = new AbilityScore(10, null);\n    public static final AbilityScores DEFAULT = new AbilityScores(TEN, TEN, TEN, TEN, TEN, TEN);\n\n    public static int getModifier(AbilityScore score) {\n        if (score.special != null) {\n            return 0;\n        }\n        return scoreToModifier(score.score);\n    }\n\n    public static int scoreToModifier(int score) {\n        int mod = score - 10;\n        if (mod % 2 != 0) {\n            mod -= 1; // round down\n        }\n        return mod / 2;\n    }\n\n    public int[] toArray() {\n        return new int[] {\n                strength.score(),\n                dexterity.score(),\n                constitution.score(),\n                intelligence.score(),\n                wisdom.score(),\n                charisma.score()\n        };\n    }\n\n    /** Strength as an ability string: `10 (+0)` */\n    public String getStr() {\n        return strength.toString();\n    }\n\n    /** Strength score as a number: 10 */\n    public int getStrStat() {\n        return strength.score();\n    }\n\n    /** Strength modifier: +1 or -2 */\n    public String getStrMod() {\n        return asModifier(getModifier(strength));\n    }\n\n    /** Dexterity as an ability string: `10 (+0)` */\n    public String getDex() {\n        return dexterity.toString();\n    }\n\n    /** Dexterity score as a number: 10 */\n    public int getDexStat() {\n        return dexterity.score();\n    }\n\n    /** Dexterity modifier: +1 or -2 */\n    public String getDexMod() {\n        return asModifier(getModifier(dexterity));\n    }\n\n    /** Constitution as an ability string: `10 (+0)` */\n    public String getCon() {\n        return constitution.toString();\n    }\n\n    /** Constitution score as a number: 10 */\n    public int getConStat() {\n        return constitution.score();\n    }\n\n    /** Constitution modifier: +1 or -2 */\n    public String getConMod() {\n        return asModifier(getModifier(constitution));\n    }\n\n    /** Intelligence as an ability string: `10 (+0)` */\n    public String getInt() {\n        return intelligence.toString();\n    }\n\n    /** Intelligence score as a number: 10 */\n    public int getIntStat() {\n        return intelligence.score();\n    }\n\n    /** Intelligence modifier: +1 or -2 */\n    public String getIntMod() {\n        return asModifier(getModifier(intelligence));\n    }\n\n    /** Wisdom as an ability string: `10 (+0)` */\n    public String getWis() {\n        return wisdom.toString();\n    }\n\n    /** Wisdom score as a number: 10 */\n    public int getWisStat() {\n        return wisdom.score();\n    }\n\n    /** Wisdom modifier: +1 or -2 */\n    public String getWisMod() {\n        return asModifier(getModifier(wisdom));\n    }\n\n    /** Charisma as an ability string: `10 (+0)` */\n    public String getCha() {\n        return charisma.toString();\n    }\n\n    /** Charisma stat as a number: 10 */\n    public int getChaStat() {\n        return charisma.score();\n    }\n\n    /** Charisma modifier: +1 or -2 */\n    public String getChaMod() {\n        return asModifier(getModifier(charisma));\n    }\n\n    public AbilityScore getScore(String name) {\n        switch (name.toLowerCase()) {\n            case \"strength\":\n                return strength;\n            case \"dexterity\":\n                return dexterity;\n            case \"constitution\":\n                return constitution;\n            case \"intelligence\":\n                return intelligence;\n            case \"wisdom\":\n                return wisdom;\n            case \"charisma\":\n                return charisma;\n            default:\n                throw new IllegalArgumentException(\"Unknown ability score: \" + name);\n        }\n    }\n\n    @Override\n    public String toString() {\n        return strength.toString()\n                + \"|\" + dexterity.toString()\n                + \"|\" + constitution.toString()\n                + \"|\" + intelligence.toString()\n                + \"|\" + wisdom.toString()\n                + \"|\" + charisma.toString();\n    }\n\n    /**\n     * Ability score. Usually an integer, but can be a special value (string) instead.\n     *\n     * @param score The ability score (integer).\n     * @param special The special value (string), or null if not applicable.\n     */\n    @TemplateData\n    public record AbilityScore(int score, String special) {\n\n        /** @return true if this score has a \"special\" value */\n        public boolean isSpecial() {\n            return special != null;\n        }\n\n        /** @return the modifier for this score as an integer */\n        public int modifier() {\n            return AbilityScores.getModifier(this);\n        }\n\n        @Override\n        public String toString() {\n            if (special != null) {\n                return special;\n            }\n            return String.format(\"%2s (%s)\", score, asModifier(modifier()));\n        }\n    }\n\n    public static class Builder {\n        AbilityScore strength;\n        AbilityScore dexterity;\n        AbilityScore constitution;\n        AbilityScore intelligence;\n        AbilityScore wisdom;\n        AbilityScore charisma;\n\n        public Builder() {\n        }\n\n        public Builder setStrength(int strength) {\n            this.strength = new AbilityScore(strength, null);\n            return this;\n        }\n\n        public Builder setDexterity(int dexterity) {\n            this.dexterity = new AbilityScore(dexterity, null);\n            return this;\n        }\n\n        public Builder setConstitution(int constitution) {\n            this.constitution = new AbilityScore(constitution, null);\n            return this;\n        }\n\n        public Builder setIntelligence(int intelligence) {\n            this.intelligence = new AbilityScore(intelligence, null);\n            return this;\n        }\n\n        public Builder setWisdom(int wisdom) {\n            this.wisdom = new AbilityScore(wisdom, null);\n            return this;\n        }\n\n        public Builder setCharisma(int charisma) {\n            this.charisma = new AbilityScore(charisma, null);\n            return this;\n        }\n\n        public AbilityScores build() {\n            return new AbilityScores(strength, dexterity, constitution, intelligence, wisdom, charisma);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/qute/AcHp.java",
    "content": "package dev.ebullient.convert.tools.dnd5e.qute;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport dev.ebullient.convert.qute.QuteUtil;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * 5eTools armor class and hit points attributes\n *\n * This data object provides a default mechanism for creating\n * a marked up string based on the attributes that are present.\n * To use it, reference it directly.\n */\n@TemplateData\npublic class AcHp implements QuteUtil {\n    /** Armor class (number) */\n    public Integer ac;\n    /** Additional armor class text. May link to related items */\n    public String acText;\n    /** Hit points */\n    public Integer hp;\n\n    /**\n     * Additional hit point text.\n     * In the case of summoned creatures, this will contain notes for how hit points\n     * should be calculated relative to the player's modifiers.\n     */\n    public String hpText;\n\n    /** Hit dice formula string: 7d10 + 14 (for creatures) */\n    public String hitDice;\n\n    public AcHp() {\n    }\n\n    public AcHp(Integer ac, String acText, Integer hp, String hpText, String hitDice) {\n        this.ac = ac;\n        this.acText = acText;\n        this.hp = hp;\n        this.hpText = hpText;\n        this.hitDice = hitDice;\n    }\n\n    public AcHp(AcHp other) {\n        this.ac = other.ac;\n        this.acText = other.acText;\n        this.hp = other.hp;\n        this.hpText = other.hpText;\n        this.hitDice = other.hitDice;\n    }\n\n    /**\n     * Hit points as a dice roller formula:\n     * \\`dice: 1d20+7|text(37)\\` (\\`1d20+7\\`)\n     */\n    public String getHpDiceRoller() {\n        return hitDice == null\n                ? getHp()\n                : \"`dice: \" + hitDice + \"|text(\" + hp + \")` (`\" + hitDice + \"`)\";\n    }\n\n    /**\n     * Hit points (number or —)\n     */\n    public String getHp() {\n        return hp == null ? hpText : hp.toString();\n    }\n\n    public String toString() {\n        List<String> out = new ArrayList<>();\n        if (isPresent(ac)) {\n            List<String> acOut = new ArrayList<>();\n            acOut.add(\"**Armor Class**\");\n            if (isPresent(ac)) {\n                acOut.add(ac.toString());\n            }\n            if (isPresent(acText)) {\n                acOut.add(\"(\" + acText + \")\");\n            }\n            out.add(\"- \" + String.join(\" \", acOut));\n        }\n        if (isPresent(hp)) {\n            List<String> hpOut = new ArrayList<>();\n            hpOut.add(\"**Hit Points**\");\n            if (isPresent(hp)) {\n                hpOut.add(hp.toString());\n                if (isPresent(hitDice)) {\n                    hpOut.add(\"(`\" + hitDice + \"`)\");\n                }\n                if (isPresent(hpText)) {\n                    hpOut.add(\"(\" + hpText + \")\");\n                }\n            } else {\n                hpOut.add(isPresent(hpText) ? hpText : \"—\");\n            }\n\n            out.add(\"- \" + String.join(\" \", hpOut));\n        }\n        return String.join(\"\\n\", out);\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/qute/ImmuneResist.java",
    "content": "package dev.ebullient.convert.tools.dnd5e.qute;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport dev.ebullient.convert.qute.QuteUtil;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * 5eTools vulnerabilities, resistances, immunities, and condition immunities\n *\n * This data object provides a default mechanism for creating\n * a marked up string based on the attributes that are present.\n * To use it, reference it directly.\n */\n@TemplateData\npublic class ImmuneResist implements QuteUtil {\n    /** Comma-separated string of creature damage vulnerabilities (if present). */\n    public String vulnerable;\n    /** Comma-separated string of creature damage resistances (if present). */\n    public String resist;\n    /** Comma-separated string of creature damage immunities (if present). */\n    public String immune;\n    /** Comma-separated string of creature condition immunities (if present). */\n    public String conditionImmune;\n\n    public ImmuneResist() {\n    }\n\n    public ImmuneResist(String vulnerable, String resist, String immune, String conditionImmune) {\n        this.vulnerable = vulnerable;\n        this.resist = resist;\n        this.immune = immune;\n        this.conditionImmune = conditionImmune;\n    }\n\n    /** True if immunities or resistances are present (otherwise false) */\n    public boolean isPresent() {\n        return isPresent(vulnerable)\n                || isPresent(resist)\n                || isPresent(immune)\n                || isPresent(conditionImmune);\n    }\n\n    @Override\n    public String toString() {\n        List<String> parts = new ArrayList<>();\n        if (isPresent(vulnerable)) {\n            parts.add(\"- **Damage Vulnerabilities** \" + vulnerable);\n        }\n        if (isPresent(resist)) {\n            parts.add(\"- **Damage Resistances** \" + resist);\n        }\n        if (isPresent(immune)) {\n            parts.add(\"- **Damage Immunities** \" + immune);\n        }\n        if (isPresent(conditionImmune)) {\n            parts.add(\"- **Condition Immunities** \" + conditionImmune);\n        }\n        return String.join(\"\\n\", parts);\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteBackground.java",
    "content": "package dev.ebullient.convert.tools.dnd5e.qute;\n\nimport java.util.List;\n\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.Tools5eSources;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * 5eTools background attributes ({@code background2md.txt}).\n *\n * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}.\n */\n@TemplateData\npublic class QuteBackground extends Tools5eQuteBase {\n    /** Formatted text listing other prerequisite conditions (optional) */\n    public final String prerequisite;\n\n    /** Formatted text listing ability score increase (optional) */\n    public final String ability;\n\n    public QuteBackground(Tools5eSources sources, String name, String source,\n            String prerequisite, String ability,\n            List<ImageRef> images, String text, Tags tags) {\n        super(sources, name, source, images, text, tags);\n        this.prerequisite = prerequisite; // optional\n        this.ability = ability; // optional\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteBastion.java",
    "content": "package dev.ebullient.convert.tools.dnd5e.qute;\n\nimport static dev.ebullient.convert.StringUtil.joinConjunct;\nimport static dev.ebullient.convert.StringUtil.toTitleCase;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.Tools5eSources;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * 5eTools background attributes ({@code bastion2md.txt}).\n *\n * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}.\n */\n@TemplateData\npublic class QuteBastion extends Tools5eQuteBase {\n    /**\n     * List of possible hirelings this bastion can have (as {@link dev.ebullient.convert.tools.dnd5e.qute.QuteBastion.Hireling},\n     * optional)\n     */\n    public final List<Hireling> hirelings;\n    /** Bastion level (optional) */\n    public final String level;\n    /** Bastion orders (optional) */\n    public final List<String> orders;\n    /**\n     * List of possible spaces this bastion can occupy (as {@link dev.ebullient.convert.tools.dnd5e.qute.QuteBastion.Space},\n     * optional)\n     */\n    public final List<Space> space;\n    /** Type */\n    public final String type;\n    /** Formatted text listing other prerequisite conditions (optional) */\n    public final String prerequisite;\n\n    public QuteBastion(Tools5eSources sources, String name, String source,\n            List<Hireling> hirelings, String level, List<String> orders,\n            String prerequisite, List<Space> space, String type,\n            String text, List<ImageRef> images, Tags tags) {\n        super(sources, name, source, images, text, tags);\n\n        this.hirelings = hirelings; // optional\n        this.level = level; // optional\n        this.orders = orders; // optional\n        this.prerequisite = prerequisite; // optional\n        this.space = space; // optional\n        this.type = type;\n    }\n\n    /** Hirelings as a descriptive string (if hirelings is present) */\n    public String getHirelingDescription() {\n        if (hirelings == null) {\n            return \"\";\n        }\n        List<String> all = new ArrayList<>();\n        for (Hireling h : hirelings) {\n            all.add(h.getDescription());\n        }\n        return joinConjunct(\" or \", all);\n    }\n\n    /** Space as a descriptive string (if space is present) */\n    public String getSpaceDescription() {\n        if (space == null) {\n            return \"\";\n        }\n        List<String> all = new ArrayList<>();\n        for (Space s : space) {\n            all.add(s.getDescription(type, getName()));\n        }\n        return joinConjunct(\" or \", all);\n    }\n\n    /**\n     * Hireling information. Either exact or min must be present.\n     *\n     * @param exact Exact number of hirelings (either exact or min)\n     * @param min Minimum number of hirelings (either exact or min)\n     * @param max Maximum number of hirelings (optional)\n     * @param space Size of bastion space required for these hirelings (optional)\n     */\n    @TemplateData\n    public record Hireling(\n            Integer exact,\n            Integer min,\n            Integer max,\n            Space space) {\n\n        /** Formatted string description of the hirelings for a Bastion */\n        public String getDescription() {\n            String spaceTxt = space == null ? \"\" : \" (%s)\".formatted(toTitleCase(space.name()));\n            // Either min or exact must be present\n            if (exact != null) {\n                return \"%s%s\".formatted(exact, spaceTxt);\n            } else if (min != null && max != null) {\n                return \"%s-%s%s\".formatted(min, max, spaceTxt);\n            } else if (min != null) {\n                return \"%s+%s\".formatted(min, spaceTxt);\n            }\n            return \"\";\n        }\n    }\n\n    /**\n     * @param name Name of this size/space\n     * @param squares Maximum number of 5-foot squares a bastion this size can occupy\n     * @param cost Cost (GP) of building a bastion of this size\n     * @param time Time to construct a bastion of this size\n     * @param prevSpace Previous space to enlarge from (optional)\n     */\n    @TemplateData\n    public record Space(\n            String name,\n            Integer squares,\n            Integer cost,\n            Integer time,\n            Space prevSpace) {\n\n        public Space(String name, Integer squares, Integer cost, Integer time) {\n            this(name, squares, cost, time, null);\n        }\n\n        /** Formatted string description of the space required for (or occupied by) a Bastion */\n        public String getDescription(String type, String facilityName) {\n            List<String> more = new ArrayList<>();\n            if (squares > 0) {\n                more.add(squares + \" sq\");\n            }\n            if (\"basic\".equalsIgnoreCase(type)) {\n                String txtCost = cost + \" GP and \" + time + \" days to add\";\n                if (prevSpace != null) {\n                    txtCost += \", or %s GP and %s days to enlarge from a %s %s\".formatted(\n                            cost - prevSpace.cost, time - prevSpace.time, prevSpace.name(), facilityName);\n                }\n                more.add(\"%s GP, %s days ^[%s]\".formatted(\n                        cost, time, txtCost));\n            }\n            return name() + (more.isEmpty() ? \"\" : \" (%s)\".formatted(String.join(\"; \", more)));\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteClass.java",
    "content": "package dev.ebullient.convert.tools.dnd5e.qute;\n\nimport static dev.ebullient.convert.StringUtil.join;\nimport static dev.ebullient.convert.StringUtil.joinConjunct;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.qute.QuteUtil;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.Tools5eSources;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * 5eTools class attributes ({@code class2md.txt})\n *\n * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}.\n */\n@TemplateData\npublic class QuteClass extends Tools5eQuteBase {\n\n    /** Formatted string describing the primary abilities for this class */\n    public final String primaryAbility;\n\n    /** Hit dice for this class as a single digit: 8 */\n    public final int hitDice;\n\n    /** Average Hit dice roll as a single digit */\n    public final int hitRollAverage;\n\n    /**\n     * Hit point die for this class as\n     * {@link dev.ebullient.convert.tools.dnd5e.qute.QuteClass.HitPointDie}\n     */\n    public final HitPointDie hitPointDie;\n\n    /** Formatted callout containing class and feature progressions. */\n    public final String classProgression;\n\n    /**\n     * Formatted text describing starting equipment as\n     * {@link dev.ebullient.convert.tools.dnd5e.qute.QuteClass.StartingEquipment}\n     */\n    public final StartingEquipment startingEquipment;\n\n    /**\n     * Multiclassing requirements and proficiencies for this class as\n     * {@link dev.ebullient.convert.tools.dnd5e.qute.QuteClass.Multiclassing}\n     */\n    public final Multiclassing multiclassing;\n\n    public QuteClass(Tools5eSources sources, String name, String source,\n            String classProgression,\n            String primaryAbility, HitPointDie hitPointDie,\n            StartingEquipment startingEquipment, Multiclassing multiclassing,\n            String text, List<ImageRef> images, Tags tags) {\n        super(sources, name, source, images, text, tags);\n        this.primaryAbility = primaryAbility;\n        this.hitPointDie = hitPointDie;\n        // compat with previous version. Sidekicks do not have a hitPointDie\n        this.hitDice = hitPointDie == null || hitPointDie.isSidekick()\n                ? 0\n                : hitPointDie.face();\n        this.hitRollAverage = hitPointDie == null || hitPointDie.isSidekick()\n                ? 0\n                : hitPointDie.average();\n        this.classProgression = classProgression;\n        this.startingEquipment = startingEquipment;\n        this.multiclassing = multiclassing;\n    }\n\n    /**\n     * Describes the multiclassing information for the class.\n     *\n     * If referenced as a unit (ignoring inner attributes), it will render\n     * formatted text describing multiclassing requirements and proficiencies.\n     *\n     * @param primaryAbility Primary ability for multiclassing as formatted\n     *        string (optional)\n     * @param requirements Prerequisites for multiclassing as formatted\n     *        string (optional)\n     * @param requirementsSpecial Special prerequisites for multiclassing as\n     *        formatted string (optional)\n     * @param skills Skill proficiencies gained as formatted string\n     *        (optional)\n     * @param weapons Weapon proficiencies gained as formatted string\n     *        (optional)\n     * @param tools Tool proficiencies gained as formatted string\n     *        (optional)\n     * @param armor Armor proficiencies gained as formatted string\n     *        (optional)\n     * @param text Formatted text describing this multiclass\n     *        (optional)\n     * @param isClassic True if this class is from the 2014 edition\n     */\n    @TemplateData\n    public record Multiclassing(\n            String primaryAbility,\n            String requirements,\n            String requirementsSpecial,\n            String skills,\n            String weapons,\n            String tools,\n            String armor,\n            String text,\n            boolean isClassic) implements QuteUtil {\n        public String prereq() {\n            if (isPresent(this.primaryAbility)) {\n                return \"To qualify for a new class, you must have a score of at least 13 in the primary ability of the new class (%s) and your current classes.\"\n                        .formatted(primaryAbility);\n            }\n            if (isPresent(requirements)) {\n                return requirements;\n            }\n            return \"\";\n        }\n\n        public String prereqSpecial() {\n            if (isPresent(requirementsSpecial)) {\n                List<String> content = new ArrayList<>();\n                if (isPresent(requirements)) {\n                    content.add(\n                            \"To qualify for a new class, you must meet the %sprerequisites for both your current class and your new one.\"\n                                    .formatted(isPresent(requirementsSpecial) ? \"\" : \"ability score \"));\n                }\n                maybeAddBlankLine(content);\n                content.add(\"**%sPrerequisites:** %s\".formatted(\n                        isPresent(requirements) ? \"Other \" : \"\",\n                        requirementsSpecial));\n                return String.join(\"\\n\", content);\n            }\n            return \"\";\n        }\n\n        public String profIntro() {\n            return \"When you gain a level in a class other than your first, you gain only some of that class's starting proficiencies.\";\n        }\n\n        @Override\n        public String toString() {\n            boolean hasRequirements = isPresent(primaryAbility) || isPresent(requirements)\n                    || isPresent(requirementsSpecial);\n            boolean hasProficiencies = isPresent(armor) || isPresent(weapons) || isPresent(tools) || isPresent(skills);\n\n            List<String> content = new ArrayList<>();\n            if (hasRequirements) {\n                content.add(prereq());\n                if (isPresent(requirementsSpecial)) {\n                    maybeAddBlankLine(content);\n                    content.add(prereqSpecial());\n                }\n            }\n            if (isPresent(text)) {\n                maybeAddBlankLine(content);\n                content.add(text);\n            }\n            if (hasProficiencies) {\n                if (isPresent(requirements)) {\n                    maybeAddBlankLine(content);\n                    content.add(profIntro());\n                }\n                maybeAddBlankLine(content);\n                if (isClassic) {\n                    if (isPresent(armor)) {\n                        content.add(\"- **Armor**: \" + armor);\n                    }\n                    if (isPresent(weapons)) {\n                        content.add(\"- **Weapons**: \" + weapons);\n                    }\n                    if (isPresent(tools)) {\n                        content.add(\"- **Tools**: \" + tools);\n                    }\n                    if (isPresent(skills)) {\n                        content.add(\"- **Skills**: \" + skills);\n                    }\n                } else {\n                    if (isPresent(skills)) {\n                        content.add(\"- **Skill Proficiencies**: \" + skills);\n                    }\n                    if (isPresent(weapons)) {\n                        content.add(\"- **Weapon Proficiencies**: \" + weapons);\n                    }\n                    if (isPresent(tools)) {\n                        content.add(\"- **Tool Proficiencies**: \" + tools);\n                    }\n                    if (isPresent(armor)) {\n                        content.add(\"- **Armor Training**: \" + armor);\n                    }\n                }\n            }\n\n            return String.join(\"\\n\", content);\n        }\n    }\n\n    /**\n     * Describes the starting equipment for the class.\n     *\n     * If referenced as a unit (ignoring inner attributes), it will render\n     * structured text describing starting proficiencies and equipment *2014* vs\n     * *2024*.\n     *\n     * @param savingThrows List of saving throws\n     * @param skills List of skills as formatted strings (links)\n     * @param weapons List of weapons as formatted strings (links)\n     * @param tools List of tools as formatted strings (links)\n     * @param armor List of armor as formatted strings (links)\n     * @param equipment List of equipment as formatted strings (links)\n     * @param isClassic True if this class is from the 2014 edition\n     */\n    @TemplateData\n    public record StartingEquipment(\n            List<String> savingThrows,\n            List<String> skills,\n            List<String> weapons,\n            List<String> tools,\n            List<String> armor,\n            String equipment,\n            boolean isClassic) implements QuteUtil {\n\n        @Override\n        public String toString() {\n            List<String> text = new ArrayList<>();\n            text.add(getProficiencies());\n            if (isPresent(equipment)) {\n                maybeAddBlankLine(text);\n                text.add((isClassic ? \"\" : \"**Starting Equipment:** \") + equipment);\n            }\n            return String.join(\"\\n\", text);\n        }\n\n        /** Formatted string of class proficiencies */\n        public String getProficiencies() {\n            List<String> text = new ArrayList<>();\n            if (isClassic) {\n                text.add(\"- **Saving Throws**: \" + getJoinOrDefault(savingThrows, null));\n                text.add(\"- **Armor**: \" + (isPresent(armor) ? getArmorString() : \"none\"));\n                text.add(\"- **Weapons**: \" + getJoinOrDefault(weapons, isClassic ? null : \" and \"));\n                text.add(\"- **Tools**: \" + getJoinOrDefault(tools, isClassic ? null : \" and \"));\n                text.add(\"- **Skills**: \" + join(\" *or* \", skills));\n            } else {\n                text.add(\"- **Saving Throw Proficiencies**: \" + getJoinOrDefault(savingThrows, null));\n                text.add(\"- **Skill Proficiencies**: \" + join(\" *or* \", skills));\n                text.add(\"- **Weapon Proficiencies**: \" + getJoinOrDefault(weapons, isClassic ? null : \" and \"));\n                if (isPresent(tools)) {\n                    text.add(\"- **Tool Proficiencies**: \" + getJoinOrDefault(tools, isClassic ? null : \" and \"));\n                }\n                if (isPresent(armor)) {\n                    text.add(\"- **Armor Training**: \" + getArmorString());\n                }\n            }\n            return String.join(\"\\n\", text);\n        }\n\n        /**\n         * Create a structured string describing armor training.\n         * Slighly different formatting and joining for 2014 vs 2024 materials.\n         *\n         * @return formatted string with links to armor item types and shield items\n         */\n        public String getArmorString() {\n            if (isClassic) {\n                return join(\", \", armor);\n            }\n            List<String> armorLinks = armor.stream()\n                    .filter(s -> s.matches(\"Light|Medium|Heavy\"))\n                    .collect(Collectors.toCollection(ArrayList::new));\n            List<String> otherLinks = armor.stream()\n                    .filter(s -> !s.matches(\"Light|Medium|Heavy\"))\n                    .toList();\n            if (armorLinks.size() > 1) {\n                // remove \" armor\" from all but the last item\n                for (int i = 0; i < armorLinks.size() - 1; i++) {\n                    armorLinks.set(i, armorLinks.get(i).replace(\" armor\", \"\"));\n                }\n                String joined = joinConjunct(\" and \", armorLinks);\n                armorLinks.clear();\n                armorLinks.add(joined);\n            }\n            armorLinks.addAll(otherLinks);\n            return joinConjunct(\" and \", armorLinks);\n        }\n\n        /**\n         * Given a list of strings, return a formatted string with a conjunction.\n         *\n         * @param value List of strings.\n         * @param conjunct Conjunction (and, or). If null, elements will be\n         *        comma-separated.\n         *        Otherwise, the first n elements comma-separated and the last\n         *        element will be joined with conjunction.\n         * @return Formatted string. If value is empty, will return \"none\".\n         */\n        public String getJoinOrDefault(List<String> value, String conjunct) {\n            if (value == null || value.isEmpty()) {\n                return \"none\";\n            }\n            return conjunct == null\n                    ? join(\", \", value)\n                    : joinConjunct(conjunct, value);\n        }\n    }\n\n    /**\n     * Describes the hit point die used by the class.\n     *\n     * If referenced as a unit (ignoring inner attributes), it will render\n     * formatted strings based on the class version (2024 or not).\n     *\n     * @param number How many dice to roll (pretty much always 1)\n     * @param face Die to roll (8, 10); This will be 0 for sidekicks\n     * @param average The average value of a hit dice roll\n     * @param isClassic True if this is a 2014 class\n     * @param isSidekick Explicit test for sidekick (alternate to 0 face)\n     */\n    @TemplateData\n    public record HitPointDie(\n            String name,\n            int number,\n            int face,\n            int average,\n            boolean isClassic,\n            boolean isSidekick) {\n        public HitPointDie(String name, int number, int face, boolean isClassic, boolean isSidekick) {\n            this(name, number, face, (number * face) / 2 + 1, isClassic, isSidekick);\n        }\n\n        @Override\n        public String toString() {\n            // return\n            // `<div><strong>Hit Point Die:</strong>\n            // ${renderer.render(Renderer.class.getHitDiceEntry(cls.hd, {styleHint}))} per\n            // ${cls.name} level</div>\n            // <div><strong>Hit Points at Level 1:</strong>\n            // ${Renderer.class.getHitPointsAtFirstLevel(cls.hd, {styleHint})}</div>\n            // <div><strong>Hit Points per additional ${cls.name} Level:</strong>\n            // ${Renderer.class.getHitPointsAtHigherLevels(cls.name, cls.hd,\n            // {styleHint})}</div>`;\n            // return styleHint === \"classic\" -- hit dice entry\n            // ? `{@dice ${clsHd.number}d${clsHd.faces}||Hit die}`\n            // : `{@dice ${clsHd.number}d${clsHd.faces}|${clsHd.number === 1 ? \"\" :\n            // clsHd.number}D${clsHd.faces}|Hit die}`;\n            if (isSidekick) {\n                String suffix = isClassic ? \"its Constitution modifier\" : \"its Con. modifier\";\n                return \"\"\"\n                        - **Hit Point Die**: *x*; specified in the sidekick's statblock (human, gnome, kobold, etc.)\n                        - **Hit Points at Level 1:** 1d*x* + %s\n                        - **Hit Points per additional %s lvel:** 1d*x* + %s (minimum of 1 hit point per level)\n                        \"\"\"\n                        .stripIndent()\n                        .formatted(suffix, name, suffix);\n            }\n\n            String dieEntry = isClassic\n                    ? \"%sd%s\".formatted(number, face)\n                    : \"%sD%s\".formatted(number == 1 ? \"\" : number, face);\n\n            // classic ? `${clsHd.number * clsHd.faces} + your Constitution modifier`\n            // : `${clsHd.number * clsHd.faces} + Con. modifier`;\n            String level1 = \"%s + %s\".formatted(\n                    number * face,\n                    isClassic ? \"your Constitution modifier\" : \"Con. modifier\");\n\n            // classic ? `${Renderer.get().render(Renderer.class.getHitDiceEntry(clsHd,\n            // {styleHint}))} (or ${((clsHd.number * clsHd.faces) / 2 + 1)}) + your\n            // Constitution modifier per ${className} level after 1st`\n            // : `${Renderer.get().render(Renderer.class.getHitDiceEntry(clsHd,\n            // {styleHint}))} + your Con. modifier, or, ${((clsHd.number * clsHd.faces) / 2\n            // + 1)} + your Con. modifier`;\n            String levelUp = isClassic\n                    ? \"%s (or %s) + your Constitution modifier\".formatted(\n                            dieEntry, average)\n                    : \"%s + your Con. modifier or %s + your Con. modifier\".formatted(\n                            dieEntry, average); // average\n\n            return \"\"\"\n                    - **Hit Point Die:** %s per %s level\n                    - **Hit Points at Level 1:** %s\n                    - **Hit Points per additional %s Level:** %s</div>\n                    \"\"\".formatted(dieEntry, name, level1, name, levelUp);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteDeck.java",
    "content": "package dev.ebullient.convert.tools.dnd5e.qute;\n\nimport java.util.List;\n\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.qute.SourceAndPage;\nimport dev.ebullient.convert.tools.CompendiumSources;\nimport dev.ebullient.convert.tools.Tags;\nimport io.quarkus.qute.TemplateData;\nimport io.quarkus.runtime.annotations.RegisterForReflection;\n\n/**\n * 5eTools deck attributes ({@code deck2md.txt})\n *\n * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}.\n */\n@TemplateData\npublic class QuteDeck extends Tools5eQuteBase {\n\n    /** Image from the back of the card as {@link dev.ebullient.convert.qute.ImageRef} (optional) */\n    public final ImageRef cardBack;\n\n    /** List of cards in the deck */\n    public final List<Card> cards;\n\n    public QuteDeck(CompendiumSources sources, String name, String source,\n            ImageRef cardBack, List<Card> cards, String text, Tags tags) {\n        super(sources, name, source, List.of(), text, tags);\n        this.cardBack = cardBack;\n        this.cards = cards;\n    }\n\n    @TemplateData\n    @RegisterForReflection\n    public static class Card {\n        /** Image from the front of the card as {@link dev.ebullient.convert.qute.ImageRef} (optional) */\n        public final ImageRef face;\n\n        /** Name of the card */\n        public final String name;\n        /** Text on the front of the card */\n        public final String text;\n\n        /** Card suit and value (optional) */\n        public final String suitValue;\n\n        /** Source and page containing card definition as {@link dev.ebullient.convert.qute.SourceAndPage} */\n        public final SourceAndPage sourceAndPage;\n\n        public Card(String name, ImageRef face, String text, String suitValue, SourceAndPage sourceAndPage) {\n            this.name = name;\n            this.face = face;\n            this.text = text;\n            this.suitValue = suitValue;\n            this.sourceAndPage = sourceAndPage;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteDeity.java",
    "content": "package dev.ebullient.convert.tools.dnd5e.qute;\n\nimport java.util.List;\n\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.Tools5eSources;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * 5eTools deity attributes ({@code deity2md.txt})\n *\n * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}.\n */\n@TemplateData\npublic class QuteDeity extends Tools5eQuteBase {\n\n    /** List of alternative names */\n    public final List<String> altNames;\n    /** Pantheon to which this deity belongs: Celtic */\n    public final String pantheon;\n    /** Alignment of this deity */\n    public final String alignment;\n    /** Title of this deity */\n    public final String title;\n    /** Category of this deity: Lesser Idols, Prime Deities */\n    public final String category;\n    /** Category of this deity: Nature, Tempest */\n    public final String domains;\n    /** Province of this deity: Discovery, Luck, Storms, Travel, ... */\n    public final String province;\n    /** Text description of deity's symbol: Wave of white water on green */\n    public final String symbol;\n    /** Image or symbol representing this deity (as {@link dev.ebullient.convert.qute.ImageRef}) */\n    public final ImageRef image;\n\n    public QuteDeity(Tools5eSources sources, String name, String source,\n            List<String> altNames, String pantheon, String alignment,\n            String title, String cateogry, String domains,\n            String province, String symbol, ImageRef symbolImg,\n            String text, Tags tags) {\n        super(sources, name, source, List.of(), text, tags);\n        this.altNames = altNames;\n        this.pantheon = pantheon;\n        this.alignment = alignment;\n        this.title = title;\n        this.category = cateogry;\n        this.domains = domains;\n        this.province = province;\n        this.symbol = symbol;\n        this.image = symbolImg;\n    }\n\n    @Override\n    public List<String> getAltNames() {\n        return altNames;\n    }\n\n    @Override\n    public String targetFile() {\n        return linkifier().getDeityResourceName(sources());\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteFeat.java",
    "content": "package dev.ebullient.convert.tools.dnd5e.qute;\n\nimport java.util.List;\n\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.Tools5eSources;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * 5eTools feat and optional feat attributes ({@code feat2md.txt})\n *\n * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}.\n */\n@TemplateData\npublic class QuteFeat extends Tools5eQuteBase {\n\n    /** Prerequisite level */\n    public final String level;\n    /** Formatted text listing other prerequisite conditions (optional) */\n    public final String prerequisite;\n    /** Feat category (optional) */\n    public final String category;\n    /** Formatted text listing ability score increase (optional) */\n    public final String ability;\n\n    public QuteFeat(Tools5eSources sources, String name, String source,\n            String prerequisite, String level, String ability, String category,\n            List<ImageRef> images, String text, Tags tags) {\n        super(sources, name, source, images, text, tags);\n        withTemplate(\"feat2md.txt\"); // Feat and OptionalFeature\n        this.level = level;\n        this.prerequisite = prerequisite; // optional\n        this.category = category; // optional\n        this.ability = ability; // optional\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteHazard.java",
    "content": "package dev.ebullient.convert.tools.dnd5e.qute;\n\nimport java.util.List;\n\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.tools.CompendiumSources;\nimport dev.ebullient.convert.tools.Tags;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * 5eTools hazard attributes ({@code hazard2md.txt})\n *\n * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}.\n */\n@TemplateData\npublic class QuteHazard extends Tools5eQuteBase {\n\n    /** Type of hazard: \"Magical Trap\", \"Wilderness Hazard\" */\n    public final String hazardType;\n\n    public QuteHazard(CompendiumSources sources, String name, String source,\n            String hazardType,\n            List<ImageRef> images, String text, Tags tags) {\n        super(sources, name, source, images, text, tags);\n        this.hazardType = hazardType;\n        withTemplate(\"hazard2md.txt\"); // not trap or hazard (types)\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteItem.java",
    "content": "package dev.ebullient.convert.tools.dnd5e.qute;\n\nimport static dev.ebullient.convert.StringUtil.join;\nimport static dev.ebullient.convert.StringUtil.toAnchorTag;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\n\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.Tools5eSources;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * 5eTools item attributes ({@code item2md.txt})\n *\n * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}.\n */\n@TemplateData\npublic class QuteItem extends Tools5eQuteBase {\n\n    /** Detailed information about this item as {@link dev.ebullient.convert.tools.dnd5e.qute.QuteItem.Variant} */\n    public final Variant rootVariant;\n\n    /** List of magic item variants as {@link dev.ebullient.convert.tools.dnd5e.qute.QuteItem.Variant}. Optional. */\n    public final List<Variant> variants;\n\n    public QuteItem(Tools5eSources sources, String source,\n            Variant rootVariant, List<Variant> variants, List<ImageRef> images,\n            String text, Tags tags) {\n        super(sources, rootVariant.name, source, images, text, tags);\n        withTemplate(\"item2md.txt\");\n\n        this.rootVariant = rootVariant;\n        this.variants = variants == null ? List.of() : variants;\n    }\n\n    @Override\n    public List<String> getAltNames() {\n        if (variants.isEmpty()) {\n            return List.of();\n        }\n        return variants.stream()\n                .map(v -> v.name())\n                .toList();\n    }\n\n    /** Formatted string of item details. Will include some combination of tier, rarity, category, and attunement */\n    public String getDetail() {\n        return rootVariant.detail();\n    }\n\n    /** Formatted string of additional item attributes. Optional. */\n    public String getSubtypeString() {\n        return rootVariant.subtypeString();\n    }\n\n    /** Formatted string listing item's properties (with links to rules if the source is present) */\n    public String getProperties() {\n        return rootVariant.getProperties();\n    }\n\n    /** Formatted string listing applicable item mastery (with links to rules if the source is present) */\n    public String getMastery() {\n        return rootVariant.getMastery();\n    }\n\n    /** Changes to armor class provided by the item, if applicable */\n    public String getArmorClass() {\n        return rootVariant.armorClass;\n    }\n\n    /** One-handed Damage string, if applicable. Contains dice formula and damage type */\n    public String getDamage() {\n        return rootVariant.damage;\n    }\n\n    /** Two-handed Damage string, if applicable. Contains dice formula and damage type */\n    public String getDamage2h() {\n        return rootVariant.damage2h;\n    }\n\n    /** Item's range, if applicable */\n    public String getRange() {\n        return rootVariant.range;\n    }\n\n    /** Strength requirement as a numerical value, if applicable */\n    public Integer getStrengthRequirement() {\n        return rootVariant.strengthRequirement;\n    }\n\n    /** True if the item imposes a stealth penalty, if applicable */\n    public boolean getStealthPenalty() {\n        return rootVariant.stealthPenalty;\n    }\n\n    /** Formatted text listing other prerequisite conditions (optional) */\n    public String getPrerequisite() {\n        return rootVariant.prerequisite;\n    }\n\n    /** Cost of the item (gp, sp, cp). Optional. */\n    public String getCost() {\n        return rootVariant.cost;\n    }\n\n    /** Cost of the item (cp) as number. Optional. */\n    public Integer getCostCp() {\n        return rootVariant.costCp;\n    }\n\n    /** Weight of the item (pounds) as a decimal value */\n    public Double getWeight() {\n        return rootVariant.weight;\n    }\n\n    /**\n     * String: list (`- \"alias\"`) of aliases for variants. Use in YAML frontmatter with `aliases:`.\n     * Will return an empty string if there are no variants\n     */\n    public String getVariantAliases() {\n        if (variants.isEmpty()) {\n            return \"\";\n        }\n        return variants.stream()\n                .map(x -> String.format(\"- \\\"%s\\\"\", x.name.replace(\"\\\"\", \"\\\\\\\"\")))\n                .collect(Collectors.joining(\"\\n\"));\n    }\n\n    /**\n     * String: list (`- [name](#anchor)`) of links to variant sections.\n     * Will return an empty string if there are no variants.\n     */\n    public String getVariantSectionLinks() {\n        if (variants.isEmpty()) {\n            return \"\";\n        }\n        return variants.stream()\n                .map(x -> String.format(\"- [%s](#%s)\", x.name, toAnchorTag(x.name)))\n                .collect(Collectors.joining(\"\\n\"));\n    }\n\n    /**\n     * @param name Name of the variant.\n     * @param detail Formatted string of item details. Will include some combination of tier, rarity, category, and attunement\n     * @param subtypeString Item subtype string. Optional.\n     * @param baseItem Markdown link to base item. Optional.\n     * @param type Item type\n     * @param typeAlt Alternate item type. Optional.\n     * @param propertiesList List of item's properties (with links to rules if the source is present).\n     * @param masteryList List of item mastery that apply to this item.\n     *\n     * @param armorClass Changes to armor class provided by the item. Optional.\n     * @param weaponCategory Weapon category. Optional. One of: \"simple\", \"martial\".\n     * @param damage One-handed Damage string. Contains dice formula and damage type. Optional.\n     * @param damage2h Two-handed Damage string. Contains dice formula and damage type. Optional.\n     * @param range Item's range. Optional.\n     * @param strengthRequirement Strength requirement as a numerical value. Optional.\n     * @param stealthPenalty True if the item imposes a stealth penalty. Optional.\n     * @param prerequisite Formatted text listing other prerequisite conditions. Optional.\n     *\n     * @param age Age/Era of item. Optional. Known values: futuristic, industrial, modern, renaissance, victorian.\n     * @param cost Cost of the item (gp, sp, cp). Usually missing for magic items.\n     * @param costCp Cost of the item (cp) as number. Usually missing for magic items.\n     * @param weight Weight of the item (pounds) as a decimal value.\n     * @param rarity Item rarity. Optional. One of: \"none\": mundane items; \"unknown (magic)\": miscellaneous magical items;\n     *        \"unknown\": miscellaneous mundane items; \"varies\": item groups or magic variants.\n     * @param tier Item tier. Optional. One of: \"minor\", \"major\".\n     * @param attunement Attunement requirements. Optional. One of: required, optional, prerequisites/conditions (implies\n     *        required).\n     *\n     * @param ammo True if this is ammunition\n     * @param cursed True if this is a cursed item\n     * @param firearm True if this is a firearm\n     * @param focus True if this is a spellcasting focus.\n     * @param focusType Spellcasting focus type. Optional. One of: \"arcane\", \"druid\", \"holy\", and/or a list of required classes.\n     * @param poison True if this is a poison.\n     * @param poisonTypes Poison type(s). Optional.\n     * @param staff True if this is a staff\n     * @param tattoo True if this is a tattoo\n     * @param wondrous True if this is a wondrous item\n     *\n     * @param bonusAc Bonus to armor class provided by the item. Optional.\n     * @param bonusWeapon Bonus to weapon attack and damage rolls provided by the item. Optional.\n     * @param bonusWeaponAttack Bonus to weapon attack rolls provided by the item. Optional.\n     * @param bonusWeaponDamage Bonus to weapon damage rolls provided by the item. Optional.\n     * @param bonusWeaponCritDamage Bonus to weapon critical damage rolls provided by the item. Optional.\n     * @param bonusSpellAttack Bonus to spell attack rolls provided by the item. Optional.\n     * @param bonusSpellDamage Bonus to spell damage rolls provided by the item. Optional.\n     * @param bonusSpellSaveDc Bonus to spell save DC provided by the item. Optional.\n     * @param bonusSavingThrow Bonus to saving throw rolls provided by the item. Optional.\n     * @param bonusAbilityCheck Bonus to ability check rolls provided by the item. Optional.\n     * @param bonusProficiencyBonus Bonus to proficiency bonus provided by the item. Optional.\n     * @param bonusSavingThrowConcentration Bonus to concentration saving throw rolls provided by the item. Optional.\n     */\n    @TemplateData\n    public static record Variant(\n            String name,\n            String detail,\n            String subtypeString,\n            String baseItem,\n            String type,\n            String typeAlt,\n            List<String> propertiesList,\n            List<String> masteryList,\n            // ---\n            String armorClass,\n            String weaponCategory,\n            String damage,\n            String damage2h,\n            String range,\n            Integer strengthRequirement,\n            boolean stealthPenalty,\n            String prerequisite,\n            // ---\n            String age,\n            String cost,\n            Integer costCp,\n            Double weight,\n            String rarity,\n            String tier,\n            String attunement,\n            // ---\n            boolean ammo,\n            boolean cursed,\n            boolean firearm,\n            boolean focus,\n            String focusType,\n            boolean poison,\n            String poisonTypes,\n            boolean staff,\n            boolean tattoo,\n            boolean wondrous,\n            // ---\n            String bonusAc,\n            String bonusWeapon,\n            String bonusWeaponAttack,\n            String bonusWeaponDamage,\n            String bonusWeaponCritDamage,\n            String bonusSpellAttack,\n            String bonusSpellDamage,\n            String bonusSpellSaveDc,\n            String bonusSavingThrow,\n            String bonusAbilityCheck,\n            String bonusProficiencyBonus,\n            String bonusSavingThrowConcentration) {\n        /** Formatted string listing item's properties (with links to rules if the source is present) */\n        public String getProperties() {\n            return join(\", \", propertiesList);\n        }\n\n        /** Formatted string listing applicable item mastery (with links to rules if the source is present) */\n        public String getMastery() {\n            return join(\", \", masteryList);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteMonster.java",
    "content": "package dev.ebullient.convert.tools.dnd5e.qute;\n\nimport static dev.ebullient.convert.StringUtil.asModifier;\nimport static dev.ebullient.convert.StringUtil.joinConjunct;\nimport static dev.ebullient.convert.StringUtil.pluralize;\nimport static dev.ebullient.convert.StringUtil.toTitleCase;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\n\nimport dev.ebullient.convert.io.JavadocIgnore;\nimport dev.ebullient.convert.io.JavadocVerbatim;\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.qute.NamedText;\nimport dev.ebullient.convert.qute.QuteUtil;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.Tools5eIndex;\nimport dev.ebullient.convert.tools.dnd5e.Tools5eIndexType;\nimport dev.ebullient.convert.tools.dnd5e.Tools5eSources;\nimport dev.ebullient.convert.tools.dnd5e.qute.AbilityScores.AbilityScore;\nimport io.quarkus.qute.TemplateData;\nimport io.quarkus.runtime.annotations.RegisterForReflection;\n\n/**\n * 5eTools creature attributes ({@code monster2md.txt})\n *\n * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}.\n */\n@TemplateData\npublic class QuteMonster extends Tools5eQuteBase {\n    private static final List<String> abilities = List.of(\"strength\", \"dexterity\", \"constitution\", \"intelligence\", \"wisdom\",\n            \"charisma\");\n\n    /** True if this is an NPC */\n    public final boolean isNpc;\n    /** Creature size (capitalized) */\n    public final String size;\n    /** Creature type (lowercase) */\n    public final String type;\n    /** Creature subtype (lowercase) */\n    public final String subtype;\n    /** Creature alignment */\n    public final String alignment;\n\n    /** Creature AC and HP as {@link dev.ebullient.convert.tools.dnd5e.qute.AcHp} */\n    public AcHp acHp;\n    /** Creature immunities and resistances as {@link dev.ebullient.convert.tools.dnd5e.qute.ImmuneResist} */\n    public final ImmuneResist immuneResist;\n    /** Creature gear as list of item links */\n    public final List<String> gear;\n\n    /** Creature speed as a comma-separated list */\n    public final String speed;\n    /** Creature ability scores as {@link dev.ebullient.convert.tools.dnd5e.qute.AbilityScores} */\n    public final AbilityScores scores;\n    /**\n     * Creature saving throws and skill modifiers as {@link dev.ebullient.convert.tools.dnd5e.qute.QuteMonster.SavesAndSkills}\n     */\n    public final SavesAndSkills savesSkills;\n    /** Comma-separated string of creature senses (if present). */\n    public final String senses;\n    /** Passive perception as a numerical value */\n    public final int passive;\n\n    /** Comma-separated string of languages the creature understands. */\n    public final String languages;\n    /** Challenge rating */\n    public final String cr;\n    /** Proficiency bonus (modifier) */\n    public final String pb;\n    /** Initiative bonus as {@link dev.ebullient.convert.tools.dnd5e.qute.QuteMonster.Initiative} */\n    public final Initiative initiative;\n    /** Creature traits as {@link dev.ebullient.convert.tools.dnd5e.qute.QuteMonster.Traits} */\n    public final Traits allTraits;\n    /** Formatted text containing the creature description. Same as `{resource.text}` */\n    public final String description;\n    /** Formatted text describing the creature's environment. Usually a single word. */\n    public final String environment;\n    /** Token image as {@link dev.ebullient.convert.qute.ImageRef} */\n    public final ImageRef token;\n\n    private final List<Spellcasting> spellcasting;\n\n    public QuteMonster(Tools5eSources sources, String name, String source, boolean isNpc, String size, String type,\n            String subtype, String alignment,\n            AcHp acHp, String speed,\n            AbilityScores scores, SavesAndSkills savesSkills, String senses, int passive,\n            ImmuneResist immuneResist, List<String> gear,\n            String languages, String cr, String pb, Initiative initiative,\n            Traits traits,\n            List<Spellcasting> spellcasting,\n            String description, String environment,\n            ImageRef tokenImage, List<ImageRef> images, Tags tags) {\n\n        super(sources, name, source, images, description, tags);\n\n        this.isNpc = isNpc;\n        this.size = size;\n        this.type = type;\n        this.subtype = subtype;\n        this.alignment = alignment;\n        this.acHp = acHp;\n        this.speed = speed;\n        this.scores = scores == null\n                ? AbilityScores.DEFAULT\n                : scores;\n        this.savesSkills = savesSkills.withParent(this);\n        this.senses = senses;\n        this.passive = passive;\n        this.immuneResist = immuneResist;\n        this.gear = gear;\n        this.languages = languages;\n        this.cr = cr;\n        this.pb = pb;\n        this.allTraits = traits;\n        this.initiative = initiative;\n        this.spellcasting = spellcasting;\n        this.description = description;\n        this.environment = environment;\n        this.token = tokenImage;\n\n        if (isPresent(spellcasting)) {\n            for (var sc : spellcasting) {\n                NamedText nt = new NamedText(sc.name, sc.getDesc());\n                if (nt.hasContent()) {\n                    switch (sc.displayAs) {\n                        case \"trait\" -> allTraits.traits().add(0, nt);\n                        case \"action\" -> allTraits.actions().add(nt);\n                        case \"bonus\" -> allTraits.bonusActions().add(nt);\n                        case \"reaction\" -> allTraits.reactions().add(nt);\n                        case \"legendary\" -> allTraits.legendaryActions().add(nt);\n                        case \"mythic\" -> allTraits.mythicActions.add(nt);\n                    }\n                }\n            }\n        }\n    }\n\n    @Override\n    public String targetPath() {\n        return linkifier().monsterPath(isNpc, type);\n    }\n\n    /** See {@link dev.ebullient.convert.tools.dnd5e.qute.AcHp#hp} */\n    public String getHp() {\n        return acHp.getHp();\n    }\n\n    /** See {@link dev.ebullient.convert.tools.dnd5e.qute.AcHp#ac} */\n    public Integer getAc() {\n        return acHp.ac;\n    }\n\n    /** See {@link dev.ebullient.convert.tools.dnd5e.qute.AcHp#acText} */\n    public String getAcText() {\n        return acHp.acText;\n    }\n\n    /** See {@link dev.ebullient.convert.tools.dnd5e.qute.AcHp#hpText} */\n    public String getHpText() {\n        return acHp.hpText;\n    }\n\n    /** See {@link dev.ebullient.convert.tools.dnd5e.qute.AcHp#hitDice} */\n    public String getHitDice() {\n        return acHp.hitDice;\n    }\n\n    /**\n     * Always returns null/empty to suppress previous default behavior that\n     * rendered spellcasting as part of traits.\n     *\n     * 2024 rules interleave spellcasting with traits, actions, bonus actions, etc.\n     *\n     * @deprecated\n     */\n    public String getSpellcasting() {\n        return null;\n    }\n\n    /**\n     * Creature spellcasting abilities as a list of {@link dev.ebullient.convert.tools.dnd5e.qute.QuteMonster.Spellcasting}\n     * attributes\n     */\n    public List<Spellcasting> getRawSpellcasting() {\n        return spellcasting;\n    }\n\n    /** See {@link dev.ebullient.convert.tools.dnd5e.qute.ImmuneResist#vulnerable} */\n    public String getVulnerable() {\n        return immuneResist.vulnerable;\n    }\n\n    /** See {@link dev.ebullient.convert.tools.dnd5e.qute.ImmuneResist#resist} */\n    public String getResist() {\n        return immuneResist.resist;\n    }\n\n    /** See {@link dev.ebullient.convert.tools.dnd5e.qute.ImmuneResist#immune} */\n    public String getImmune() {\n        return immuneResist.immune;\n    }\n\n    /** See {@link dev.ebullient.convert.tools.dnd5e.qute.ImmuneResist#conditionImmune} */\n    public String getConditionImmune() {\n        return immuneResist.conditionImmune;\n    }\n\n    /** Creature type (lowercase) and subtype if present: `{resource.type} ({resource.subtype})` */\n    public String getFullType() {\n        return type + ((subtype == null || subtype.isEmpty()) ? \"\" : \" (\" + subtype + \")\");\n    }\n\n    @Deprecated\n    public String getScoreString() {\n        return scores.toString();\n    }\n\n    /**\n     * String representation of saving throws.\n     * Equivalent to `{resource.savesSkills.saves}`\n     */\n    public String getSavingThrows() {\n        if (savesSkills == null) {\n            return null;\n        }\n        return savesSkills.getSaves();\n    }\n\n    /**\n     * String representation of saving throws.\n     * Equivalent to `{resource.savesSkills.skills}`\n     */\n    public String getSkills() {\n        if (savesSkills == null) {\n            return null;\n        }\n        return savesSkills.getSkills();\n    }\n\n    /** Creature traits as a list of {@link dev.ebullient.convert.qute.NamedText} */\n    public List<NamedText> getTrait() {\n        return traitsWithHeader(allTraits.traits);\n    }\n\n    /** Creature actions as a list of {@link dev.ebullient.convert.qute.NamedText} */\n    public List<NamedText> getAction() {\n        return traitsWithHeader(allTraits.actions);\n    }\n\n    /** Creature bonus actions as a list of {@link dev.ebullient.convert.qute.NamedText} */\n    public List<NamedText> getBonusAction() {\n        return traitsWithHeader(allTraits.bonusActions);\n    }\n\n    /** Creature reactions as a list of {@link dev.ebullient.convert.qute.NamedText} */\n    public List<NamedText> getReaction() {\n        return traitsWithHeader(allTraits.reactions);\n    }\n\n    /** Creature legendary traits as a list of {@link dev.ebullient.convert.qute.NamedText} */\n    public List<NamedText> getLegendary() {\n        return traitsWithHeader(allTraits.legendaryActions);\n    }\n\n    private List<NamedText> traitsWithHeader(TraitDescription traitDesc) {\n        if (isPresent(traitDesc) && traitDesc.isPresent()) {\n            List<NamedText> traits = new ArrayList<>();\n            if (isPresent(traitDesc.description())) {\n                traits.add(new NamedText(\"\", traitDesc.description()));\n            }\n            traits.addAll(traitDesc.traits());\n            return traits;\n        }\n        return List.of();\n    }\n\n    /**\n     * Map of grouped legendary traits (Lair Actions, Regional Effects, etc.).\n     * The key the group name, and the value is a list of {@link dev.ebullient.convert.qute.NamedText}.\n     */\n    public Collection<NamedText> getLegendaryGroup() {\n        List<NamedText> legendaryGroupTraits = new ArrayList<>();\n        if (isPresent(allTraits.lairActions) && allTraits.lairActions.isPresent()) {\n            legendaryGroupTraits.add(allTraits.lairActions.asNamedText());\n        }\n        if (isPresent(allTraits.regionalEffects) && allTraits.regionalEffects.isPresent()) {\n            legendaryGroupTraits.add(allTraits.regionalEffects.asNamedText());\n        }\n        if (isPresent(allTraits.mythicActions) && allTraits.mythicActions.isPresent()) {\n            legendaryGroupTraits.add(allTraits.mythicActions.asNamedText());\n        }\n        return legendaryGroupTraits;\n    }\n\n    /**\n     * Markdown link to legendary group (can be embedded).\n     */\n    public String getLegendaryGroupLink() {\n        return allTraits.legendaryGroupLink();\n    }\n\n    /**\n     * A minimal YAML snippet containing monster attributes required by the\n     * Initiative Tracker plugin. Use this in frontmatter.\n     *\n     * The source book will not be included in the monster name.\n     */\n    public String get5eInitiativeYamlNoSource() {\n        return get5eInitiativeYaml(false);\n    }\n\n    /**\n     * A minimal YAML snippet containing monster attributes required by the\n     * Initiative Tracker plugin. Use this in frontmatter.\n     *\n     * The source book will be included in the name if it isn't the default monster source (\"MM\").\n     */\n    public String get5eInitiativeYaml() {\n        return get5eInitiativeYaml(true);\n    }\n\n    private String get5eInitiativeYaml(boolean withSource) {\n        Map<String, Object> map = new LinkedHashMap<>();\n        addUnlessEmpty(map, \"name\", name + yamlMonsterName(withSource));\n        addIntegerUnlessEmpty(map, \"ac\", acHp.ac);\n        addIntegerUnlessEmpty(map, \"hp\", acHp.hp);\n        if (initiative != null) {\n            map.put(\"modifier\", initiative.bonus);\n            // TODO: passive initiative\n        }\n        addUnlessEmpty(map, \"hit_dice\", acHp.hitDice);\n        addUnlessEmpty(map, \"cr\", cr);\n        map.put(\"stats\", scores.toArray()); // for initiative\n        addUnlessEmpty(map, \"source\", getBooks());\n        return Tui.plainYaml().dump(map).trim();\n    }\n\n    /**\n     * Complete monster attributes in the format required by the Fantasy statblock plugin.\n     * Uses double-quoted syntax to deal with a variety of characters occuring in\n     * trait descriptions. Usable in frontmatter or Fantasy Statblock code blocks.\n     *\n     * The source book will not be included in the monster name.\n     */\n    public String get5eStatblockYamlNoSource() {\n        return render5eStatblockYaml(false);\n    }\n\n    /**\n     * Complete monster attributes in the format required by the Fantasy statblock plugin.\n     * Uses double-quoted syntax to deal with a variety of characters occuring in\n     * trait descriptions. Usable in frontmatter or Fantasy Statblock code blocks.\n     *\n     * The source book will be included in the name if it isn't the default monster source (\"MM\").\n     */\n    public String get5eStatblockYaml() {\n        return render5eStatblockYaml(true);\n    }\n\n    private String render5eStatblockYaml(boolean withSource) {\n        // Map our types to the fields and values that Fantasy Statblock expects\n\n        Map<String, Object> map = new LinkedHashMap<>();\n        addUnlessEmpty(map, \"name\", name + yamlMonsterName(withSource));\n        addUnlessEmpty(map, \"size\", size);\n        addUnlessEmpty(map, \"type\", type);\n        addUnlessEmpty(map, \"subtype\", subtype);\n        addUnlessEmpty(map, \"alignment\", alignment);\n\n        addIntegerUnlessEmpty(map, \"ac\", acHp.ac);\n        addUnlessEmpty(map, \"ac_class\", acHp.acText);\n        addIntegerUnlessEmpty(map, \"hp\", acHp.hp);\n        addUnlessEmpty(map, \"hit_dice\", acHp.hitDice);\n\n        if (initiative != null) {\n            map.put(\"modifier\", initiative.bonus);\n            // TODO: passive initiative?\n        }\n\n        map.put(\"stats\", scores.toArray());\n        addUnlessEmpty(map, \"speed\", speed);\n\n        if (savesSkills != null) {\n            if (isPresent(savesSkills.saves)) {\n                map.put(\"saves\", savesSkills.getSaveValues());\n            }\n            if (isPresent(savesSkills.skills) || isPresent(savesSkills.skillChoices)) {\n                map.put(\"skillsaves\", savesSkills.getSkillValues());\n            }\n        }\n        addUnlessEmpty(map, \"damage_vulnerabilities\", immuneResist.vulnerable);\n        addUnlessEmpty(map, \"damage_resistances\", immuneResist.resist);\n        addUnlessEmpty(map, \"damage_immunities\", immuneResist.immune);\n        addUnlessEmpty(map, \"condition_immunities\", immuneResist.conditionImmune);\n        addUnlessEmpty(map, \"gear\", gear);\n        map.put(\"senses\", (senses.isBlank() ? \"\" : senses + \", \") + \"passive Perception \" + passive);\n        map.put(\"languages\", languages);\n        addUnlessEmpty(map, \"cr\", cr);\n\n        addUnlessEmpty(map, \"traits\", traitsFrom(allTraits.traits()));\n        addUnlessEmpty(map, \"actions\", traitsFrom(allTraits.actions()));\n        addUnlessEmpty(map, \"bonus_actions\", traitsFrom(allTraits.bonusActions()));\n        addUnlessEmpty(map, \"reactions\", traitsFrom(allTraits.reactions()));\n        addUnlessEmpty(map, \"lair_actions\", traitsFrom(allTraits.lairActions()));\n        addUnlessEmpty(map, \"regional_effects\", traitsFrom(allTraits.regionalEffects()));\n\n        TraitDescription legendary = allTraits.legendaryActions();\n        if (isPresent(legendary)) {\n            addUnlessEmpty(map, \"legendary_description\", legendary.description());\n            addUnlessEmpty(map, \"legendary_actions\", legendary.traits());\n        }\n\n        TraitDescription mythic = allTraits.mythicActions();\n        if (isPresent(mythic)) {\n            addUnlessEmpty(map, \"mythic_description\", mythic.description());\n            addUnlessEmpty(map, \"mythic_actions\", mythic.traits());\n        }\n\n        addUnlessEmpty(map, \"source\", getBooks());\n        if (token != null) {\n            map.put(\"image\", token.getVaultPath());\n        }\n\n        // De-markdown-ify\n        return Tui.quotedYaml().dump(map).trim()\n                .replaceAll(\"`\", \"\");\n    }\n\n    private List<NamedText> traitsFrom(TraitDescription traitDesc) {\n        if (isPresent(traitDesc) && isPresent(traitDesc.traits())) {\n            return traitDesc.traits();\n        }\n        return null;\n    }\n\n    private String yamlMonsterName(boolean withSource) {\n        if (withSource) {\n            String source = getBooks().get(0);\n            String outputSource = Tools5eIndexType.monster.defaultOutputSource();\n            if (!outputSource.equalsIgnoreCase(source)) {\n                return \" (\" + source + \")\";\n            }\n        }\n        return \"\";\n    }\n\n    /**\n     * 5eTools creature traits.\n     *\n     * @param traits Creature traits as a list of {@link dev.ebullient.convert.qute.NamedText}\n     * @param actions Creature actions as a list of {@link dev.ebullient.convert.qute.NamedText}\n     * @param bonusActions Creature bonus actions as a list of {@link dev.ebullient.convert.qute.NamedText}\n     * @param reactions Creature reactions as a list of {@link dev.ebullient.convert.qute.NamedText}\n     * @param legendaryActions Creature legendary traits as a list of {@link dev.ebullient.convert.qute.NamedText}\n     * @param lairActions Creature lair actions as a list of {@link dev.ebullient.convert.qute.NamedText}\n     * @param regionalEffects Creature regional effects as a list of {@link dev.ebullient.convert.qute.NamedText}\n     * @param mythicActions Creature mythic traits as a list of {@link dev.ebullient.convert.qute.NamedText}\n     * @param legendaryGroupLink Link to the legendary group, if present\n     * @param legendaryActionCount Number of legendary actions\n     * @param legendaryActionsLairCount Number of legendary lair actions\n     */\n    @TemplateData\n    @RegisterForReflection\n    public record Traits(\n            TraitDescription traits,\n            TraitDescription actions,\n            TraitDescription bonusActions,\n            TraitDescription reactions,\n            TraitDescription legendaryActions,\n            TraitDescription lairActions,\n            TraitDescription regionalEffects,\n            TraitDescription mythicActions,\n            String legendaryGroupLink) implements QuteUtil {\n    }\n\n    /**\n     * 5eTools creature trait description.\n     *\n     * @param title Title of the trait description\n     * @param description Formatted text describing the collection of traits\n     * @param traits Traits as a list of {@link dev.ebullient.convert.qute.NamedText}\n     */\n    @TemplateData\n    @RegisterForReflection\n    public record TraitDescription(\n            String title,\n            String description,\n            List<NamedText> traits) implements QuteUtil {\n\n        public void add(int i, NamedText nt) {\n            traits.add(0, nt);\n        }\n\n        public boolean isPresent() {\n            return isPresent(description) || isPresent(traits);\n        }\n\n        public void add(NamedText nt) {\n            traits.add(nt);\n        }\n\n        public NamedText asNamedText() {\n            List<String> text = new ArrayList<>();\n            if (isPresent(description)) {\n                text.add(description);\n                if (isPresent(traits)) {\n                    text.add(\"\");\n                }\n            }\n            for (var nt : traits) {\n                text.add(nt.toString());\n            }\n            return new NamedText(title, text, traits);\n        }\n    }\n\n    /**\n     * 5eTools creature spellcasting attributes.\n     *\n     * This data object provides a default mechanism for creating\n     * a marked up string based on the attributes that are present.\n     *\n     * To use it, reference it directly:\n     *\n     * ```md\n     * {#for spellcasting in resource.spellcasting}\n     * {spellcasting}\n     * {/for}\n     * ```\n     *\n     * or, using `{#each}` instead:\n     *\n     * ```md\n     * {#each resource.spellcasting}\n     * {it}\n     * {/each}\n     * ```\n     */\n    @TemplateData\n    @RegisterForReflection\n    public static class Spellcasting implements QuteUtil {\n        /** Name: \"Spellcasting\" or \"Innate Spellcasting\" */\n        public String name;\n        /** Formatted text that should be printed before the list of spells */\n        public List<String> headerEntries;\n\n        /** Spells (links) that can be cast a fixed number of times (constant), at will (will), or as a ritual */\n        public Map<String, List<String>> fixed;\n\n        /**\n         * Map of frequency to spells (links).\n         *\n         * Frequencies (key)\n         * - charges\n         * - daily\n         * - legendary\n         * - monthly\n         * - recharge\n         * - rest\n         * - restLong\n         * - weekly\n         * - yearly\n         *\n         * Value is another map containing additional key/value pairs, where the key is a number,\n         * and the value is a list of spells (links).\n         *\n         * If the key ends with `e` (like `1e` or `2e`), each will be appended, e.g. \"1/day each\"\n         * to specify that each spell can be cast once per day.\n         */\n        @JavadocVerbatim\n        public Map<String, Map<String, List<String>>> variable;\n\n        /**\n         * Map: key = spell level, value: spell level information as\n         * {@link dev.ebullient.convert.tools.dnd5e.qute.QuteMonster.Spells}\n         */\n        public Map<String, Spells> spells;\n        /** Formatted text that should be printed after the list of spells */\n        public List<String> footerEntries;\n        public String ability;\n\n        /**\n         * Groups that should be hidden. Values: `constant`, `will`, `rest`, `restLong`, `daily`, `weekly`, `monthly`, `yearly`,\n         * `ritual`, `spells`, `charges`, `recharge`, `legendary`\n         */\n        public List<String> hidden;\n\n        /**\n         * Attribute should be displayed as specified trait type. Values: `trait` (default), `action`, `bonus`, `reaction`,\n         * `legendary`\n         */\n        public String displayAs = \"trait\";\n\n        @Override\n        public String toString() {\n            return \"%s%s\".formatted(\n                    isPresent(name) ? (\"***\" + name + \".*** \") : \"\",\n                    getDesc());\n\n        }\n\n        /** Formatted description: renders all attributes (except name) unless the trait is hidden */\n        public String getDesc() {\n            List<String> text = new ArrayList<>();\n            if (!headerEntries.isEmpty()) {\n                text.addAll(headerEntries);\n                text.add(\"\");\n            }\n\n            if (fixed.containsKey(\"constant\") && !hidden.contains(\"constant\")) {\n                appendList(text, \"Constant\", fixed.get(\"constant\"));\n            }\n            if (fixed.containsKey(\"will\") && !hidden.contains(\"will\")) {\n                appendList(text, \"At will\", fixed.get(\"will\"));\n            }\n            for (var duration : DurationType.values()) {\n                String key = duration.name();\n                if (hidden.contains(key)) {\n                    continue;\n                }\n                if (variable.containsKey(key) && !hidden.contains(key)) {\n                    Map<String, List<String>> v = variable.get(key);\n                    switch (duration) {\n                        case recharge -> {\n                            Function<String, String> f = (num) -> {\n                                // This is a {@recharge} tag\n                                return Tools5eIndex.instance().replaceText(String.format(duration.durationText, num));\n                            };\n                            appendList(text, f, v);\n                        }\n                        case charges, legendary -> {\n                            Function<String, String> f = (num) -> {\n                                boolean isEach = num.endsWith(\"e\");\n                                String value = String.format(duration.durationText, num.replace(\"e\", \"\"));\n                                return isEach\n                                        ? pluralize(value, num.replace(\"e\", \"\")) + \" each\"\n                                        : pluralize(value, num);\n                            };\n                            appendList(text, f, v);\n                        }\n                        default -> {\n                            Function<String, String> f = (num) -> {\n                                boolean isEach = num.endsWith(\"e\");\n                                String value = String.format(duration.durationText, num.replace(\"e\", \"\"));\n                                return isEach\n                                        ? value + \" each\"\n                                        : value;\n                            };\n                            appendList(text, f, v);\n                        }\n                    }\n                }\n            }\n\n            if (fixed.containsKey(\"ritual\") && !hidden.contains(\"ritual\")) {\n                appendList(text, \"Rituals\", fixed.get(\"ritual\"));\n            }\n\n            if (isPresent(spells) && !hidden.contains(\"spells\")) {\n                spells.forEach((k, v) -> appendList(text, spellToTitle(k, v), v.spells));\n            }\n\n            if (!footerEntries.isEmpty()) {\n                text.add(\"\");\n                text.addAll(footerEntries);\n            }\n            return String.join(\"\\n\", text);\n        }\n\n        String spellToTitle(String key, Spells spells) {\n            if (\"0\".equals(key)) {\n                return \"Cantrips (at will)\";\n            }\n            if (spells.lowerBound > 0) {\n                return String.format(\"%s-%s level%s\",\n                        levelToString(spells.lowerBound + \"\"),\n                        levelToString(key),\n                        spellSlots(key, spells));\n            } else {\n                return String.format(\"%s level%s\",\n                        levelToString(key),\n                        spellSlots(key, spells));\n            }\n        }\n\n        String spellSlots(String key, Spells spells) {\n            if (spells.slots > 0) {\n                return String.format(\" (%s slots)\", spells.slots);\n            }\n            return \"\";\n        }\n\n        void appendList(List<String> text, String title, List<String> spells) {\n            if (spells == null || spells.isEmpty()) {\n                return;\n            }\n            maybeAddBlankLine(text);\n            text.add(String.format(\"**%s:** %s\", title, String.join(\", \", spells)));\n        }\n\n        void appendList(List<String> text, Function<String, String> titleFunction, Map<String, List<String>> spells) {\n            if (spells == null || spells.isEmpty()) {\n                return;\n            }\n            for (int i = 9; i > 0; i--) {\n                String key = String.valueOf(i);\n                List<String> spellList = spells.get(key);\n                if (isPresent(spellList)) {\n                    appendList(text, titleFunction.apply(key), spellList);\n                }\n\n                key = key + \"e\";\n                spellList = spells.get(key);\n                if (isPresent(spellList)) {\n                    appendList(text, titleFunction.apply(key), spellList);\n                }\n            }\n        }\n    }\n\n    /**\n     * 5eTools creature spell attributes (associated with a spell level)\n     */\n    @TemplateData\n    public static class Spells {\n        /** Available spell slots */\n        public int slots;\n        /** Set if this represents a spell range (the associated key is the upper bound) */\n        public int lowerBound;\n        /** List of spells (links) */\n        public List<String> spells;\n    }\n\n    /**\n     * Saving throw modifier.\n     *\n     * Usually an integer, but may be a \"special\" value (string, homebrew).\n     *\n     * @param ability Ability name, will be null if \"special\"\n     * @param modifier Modifier value. Will be 0 if unset or \"special\"\n     * @param special Either the \"special\" value or a non-numeric modifier value\n     */\n    @TemplateData\n    public record SavingThrow(String ability, int modifier, String special) implements QuteUtil {\n\n        public SavingThrow(String ability, String special) {\n            this(\"special\".equalsIgnoreCase(ability) ? null : ability,\n                    0, special);\n        }\n\n        public SavingThrow(String ability, int modifier) {\n            this(\"special\".equalsIgnoreCase(ability) ? null : ability,\n                    modifier, null);\n        }\n\n        public SavingThrow(String ability, AbilityScore score) {\n            this(ability, score.modifier(), score.special());\n        }\n\n        /** @return true if this saving throw has a \"special\" value */\n        public boolean isSpecial() {\n            return special != null;\n        }\n\n        public Object mapValue() {\n            return isSpecial() ? special : asModifier(modifier);\n        }\n\n        @Override\n        public String toString() {\n            if (isSpecial()) {\n                return Tools5eIndex.instance().replaceText(special);\n            }\n            return \"%s %s\".formatted(ability, asModifier(modifier));\n        }\n    }\n\n    /**\n     * Skill modifier.\n     *\n     * Usually an integer, but may be a \"special\" value (string, homebrew).\n     *\n     * @param skill Skill name, will be null if \"special\"\n     * @param skillLink Skill name as a link, will be null if \"special\"\n     * @param modifier Modifier value. Will be 0 if unset or \"special\"\n     * @param special Either the \"special\" value or a non-numeric modifier value\n     */\n    @TemplateData\n    public record SkillModifier(String skill, String skillLink, int modifier, String special) {\n        public SkillModifier(String skill, String skillLink, String special) {\n            this(skill, skillLink, 0, special);\n        }\n\n        public SkillModifier(String skill, String skillLink, int modifier) {\n            this(skill, skillLink, modifier, null);\n        }\n\n        /** @return true if this saving throw has a \"special\" value */\n        public boolean isSpecial() {\n            return special != null;\n        }\n\n        public Object mapValue() {\n            return isSpecial() ? special : asModifier(modifier);\n        }\n\n        @Override\n        public String toString() {\n            if (isSpecial()) {\n                return Tools5eIndex.instance().replaceText(special);\n            }\n            return \"%s %s\".formatted(skillLink, asModifier(modifier));\n        }\n    }\n\n    /**\n     * 5eTools creature saving throws and skill attributes.\n     */\n    @TemplateData\n    @RegisterForReflection\n    public static class SavesAndSkills implements QuteUtil {\n        @JsonIgnore\n        private QuteMonster parent;\n\n        private SavesAndSkills withParent(QuteMonster parent) {\n            this.parent = parent;\n            return this;\n        }\n\n        /**\n         * Creature saving throws as a list of {@link dev.ebullient.convert.tools.dnd5e.qute.QuteMonster.SavingThrow}.\n         */\n        public List<SavingThrow> saves;\n\n        /**\n         * Creature skill modifiers as a list of {@link dev.ebullient.convert.tools.dnd5e.qute.QuteMonster.SkillModifier}.\n         */\n        public List<SkillModifier> skills;\n\n        /**\n         * Sometimes creatures have choices (one of the following...)\n         * This is a list of lists of {@link dev.ebullient.convert.tools.dnd5e.qute.QuteMonster.SkillModifier},\n         * where each sublist is a a group to choose from.\n         */\n        public List<List<SkillModifier>> skillChoices;\n\n        /** Creature saving throws as a list: Constitution +6, Intelligence +8 */\n        public String getSaves() {\n            if (!isPresent(saves)) {\n                return \"\";\n            }\n            return saves.stream()\n                    .map(SavingThrow::toString)\n                    .collect(Collectors.joining(\", \"));\n        }\n\n        /** Saving throws as a list of maps (for YAML Statblock) */\n        public List<Map<String, Object>> getSaveValues() {\n            if (!isPresent(saves)) {\n                return List.of();\n            }\n            return saves.stream()\n                    .map(s -> {\n                        Map<String, Object> map = new LinkedHashMap<>();\n                        if (s.isSpecial()) {\n                            String ability = s.ability();\n                            if (ability != null) {\n                                map.put(\"name\", ability);\n                            }\n                            map.put(\"desc\", s.mapValue());\n                        } else {\n                            map.put(s.ability().toLowerCase(), s.modifier());\n                        }\n                        return map;\n                    })\n                    .collect(Collectors.toList());\n        }\n\n        /** Saving throws as a list of maps (for YAML Statblock) */\n        public Map<String, Object> getSaveOrDefault() {\n            Map<String, Object> saveDefaults = new HashMap<>();\n            for (String ability : abilities) {\n                Optional<SavingThrow> optSt = Optional.empty();\n                if (isPresent(saves)) {\n                    optSt = saves.stream()\n                            .filter(st -> st.ability().equalsIgnoreCase(ability))\n                            .findFirst();\n                }\n\n                if (optSt.isEmpty()) {\n                    AbilityScore score = parent.scores.getScore(ability);\n                    if (score != null) {\n                        addToMap(saveDefaults, new SavingThrow(ability, score), true);\n                    } else {\n                        addToMap(saveDefaults, new SavingThrow(getSkills(), \"⏤\"), true);\n                    }\n                } else {\n                    addToMap(saveDefaults, optSt.get(), false);\n                }\n            }\n            return saveDefaults;\n        }\n\n        private void addToMap(Map<String, Object> map, SavingThrow s, boolean isDefault) {\n            map.put(s.ability().toLowerCase(),\n                    (isDefault ? \"%s\" : \"**%s**\").formatted(s.mapValue()));\n        }\n\n        /**\n         * Creature skills as a list (with links)\n         *\n         * - `[History](..) +12, [Perception](...) +12`\n         * - `[History](..) +12; [Perception](...) +12; _One of_ [Athletics](...) +12 or [Acrobatics](...) +12`\n         *\n         */\n        public String getSkills() {\n            if (!isPresent(skills) && !isPresent(skillChoices)) {\n                return \"\";\n            }\n            String separator = \", \";\n            List<String> text = new ArrayList<>();\n            if (isPresent(skills)) {\n                text.addAll(skills.stream()\n                        .map(SkillModifier::toString)\n                        .collect(Collectors.toList()));\n            }\n            List<String> choices = flattenChoices();\n            if (isPresent(choices)) {\n                text.addAll(choices);\n                separator = \"; \";\n            }\n            return String.join(separator, text);\n        }\n\n        /** Skill modifiers as a list of maps (for YAML Statblock) */\n        public List<Map<String, Object>> getSkillValues() {\n            if (!isPresent(skills) && !isPresent(skillChoices)) {\n                return List.of();\n            }\n            List<Map<String, Object>> skillList = new ArrayList<>();\n            for (SkillModifier s : skills) {\n                Map<String, Object> map = new LinkedHashMap<>();\n                String name = s.skillLink();\n                if (name != null) {\n                    map.put(\"name\", name);\n                }\n                map.put(\"desc\", s.mapValue());\n                skillList.add(map);\n            }\n            List<String> choices = flattenChoices();\n            if (isPresent(choices)) {\n                for (String choice : choices) {\n                    Map<String, Object> map = new LinkedHashMap<>();\n                    map.put(\"desc\", choice);\n                    skillList.add(map);\n                }\n            }\n            return skillList;\n        }\n\n        public List<String> flattenChoices() {\n            if (skillChoices == null || skillChoices.isEmpty()) {\n                return List.of();\n            }\n            List<String> text = new ArrayList<>();\n            for (List<SkillModifier> chooseOneSkill : skillChoices) {\n                if (chooseOneSkill.isEmpty()) {\n                    continue;\n                }\n                List<String> inner = new ArrayList<>();\n                inner.addAll(chooseOneSkill.stream()\n                        .map(SkillModifier::toString)\n                        .collect(Collectors.toList()));\n                text.add(\"\\n\\nOne of \" + joinConjunct(\" or \", inner));\n            }\n            return text;\n        }\n    }\n\n    /**\n     * 5eTools creature initiative attributes.\n     *\n     * @param bonus Initiative modifier\n     * @param mode Initiative mode: \"advantage\", \"disadvantage\", or \"none\"\n     * @param passive Passive initiative value (number)\n     */\n    @TemplateData\n    public record Initiative(int bonus, InitiativeMode mode, int passive) {\n        public Initiative(int bonus) {\n            this(bonus, InitiativeMode.none, 10 + bonus);\n        }\n\n        public Initiative(int bonus, InitiativeMode mode) {\n            // const advDisMod = mon.initiative.advantageMode === \"adv\" ? 5 : mon.initiative.advantageMode === \"dis\" ? -5 : 0;\n            // return 10 + initBonus + advDisMod;\n            this(bonus, mode,\n                    10 + bonus +\n                            (mode == InitiativeMode.advantage\n                                    ? 5\n                                    : mode == InitiativeMode.disadvantage ? -5 : 0));\n        }\n\n        /** String representation of passive initiative value */\n        public String getPassiveInitiative() {\n            if (mode == InitiativeMode.none) {\n                return \"`\" + passive + \"`\";\n            } else {\n                return \"<span title=\\\"This creature has %s on Initiative.\\\">`%s`</span>\".formatted(\n                        toTitleCase(mode.name()), passive);\n            }\n        }\n\n        public String toString() {\n            var index = Tools5eIndex.instance();\n            return index.replaceText(\"{@initiative %s} (%s)\".formatted(\n                    bonus,\n                    getPassiveInitiative()));\n        }\n    }\n\n    /**\n     * Initiative mode: \"advantage\", \"disadvantage\", or \"none\"\n     */\n    public enum InitiativeMode {\n        /** Creature rolls initiative with advantage */\n        advantage,\n        /** Creature rolls initiative with disadvantage */\n        disadvantage,\n        /** Creature rolls initiative normally (default) */\n        none;\n\n        public static InitiativeMode fromString(String string) {\n            if (string == null || string.isBlank()) {\n                return none;\n            }\n            var lower = string.toLowerCase();\n            if (lower.startsWith(\"adv\")) {\n                return advantage;\n            } else if (lower.startsWith(\"dis\")) {\n                return disadvantage;\n            }\n            return none;\n        }\n    }\n\n    @JavadocIgnore\n    @TemplateData\n    public enum HiddenType {\n        constant,\n        will,\n        rest,\n        restLong,\n        daily,\n        weekly,\n        monthly,\n        yearly,\n        ritual,\n        spells,\n        charges,\n        recharge,\n        legendary,\n    }\n\n    @JavadocIgnore\n    @TemplateData\n    public enum DurationType {\n        recharge(\"{@recharge %s}\"),\n        legendary(\"%s legendary action\"),\n        charges(\"%s charge\"),\n        rest(\"%s/rest\"),\n        restLong(\"%s/long rest\"),\n        daily(\"%s/day\"),\n        weekly(\"%s/week\"),\n        monthly(\"%s/month\"),\n        yearly(\"%s/year\"),;\n\n        final String durationText;\n\n        DurationType(String durationText) {\n            this.durationText = durationText;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteObject.java",
    "content": "package dev.ebullient.convert.tools.dnd5e.qute;\n\nimport java.util.Collection;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.qute.NamedText;\nimport dev.ebullient.convert.tools.CompendiumSources;\nimport dev.ebullient.convert.tools.Tags;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * 5eTools object attributes ({@code object2md.txt})\n *\n * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}.\n */\n@TemplateData\npublic class QuteObject extends Tools5eQuteBase {\n    /** True if this is an NPC */\n    public final boolean isNpc;\n    /** Object size (capitalized) */\n    public final String size;\n    /** Creature type (lowercase); optional */\n    public final String creatureType;\n    /** Object type */\n    public final String objectType;\n\n    /** Object AC and HP as {@link dev.ebullient.convert.tools.dnd5e.qute.AcHp} */\n    public AcHp acHp;\n    /** Object immunities and resistances as {@link dev.ebullient.convert.tools.dnd5e.qute.ImmuneResist} */\n    public final ImmuneResist immuneResist;\n\n    /** Object speed as a comma-separated list */\n    public final String speed;\n    /** Object ability scores as {@link dev.ebullient.convert.tools.dnd5e.qute.AbilityScores}) */\n    public final AbilityScores scores;\n    /** Comma-separated string of object senses (if present). */\n    public final String senses;\n\n    /** Object actions as a list of {@link dev.ebullient.convert.qute.NamedText} */\n    public final Collection<NamedText> action;\n\n    /** Token image as {@link dev.ebullient.convert.qute.ImageRef} */\n    public final ImageRef token;\n\n    public QuteObject(CompendiumSources sources,\n            String name, String source,\n            boolean isNpc, String size,\n            String creatureType, String objectType,\n            AcHp acHp, String speed,\n            AbilityScores scores,\n            String senses,\n            ImmuneResist immuneResist,\n            Collection<NamedText> actions,\n            ImageRef tokenImage, List<ImageRef> images,\n            String text, Tags tags) {\n        super(sources, name, source, images, text, tags);\n        this.isNpc = isNpc;\n        this.size = size;\n        this.creatureType = creatureType;\n        this.objectType = objectType;\n\n        this.acHp = acHp;\n        this.immuneResist = immuneResist;\n\n        this.speed = speed;\n        this.scores = scores;\n        this.senses = senses;\n        this.action = actions;\n\n        this.token = tokenImage;\n    }\n\n    /** See {@link dev.ebullient.convert.tools.dnd5e.qute.AcHp#hp} */\n    public String getHp() {\n        return acHp.getHp();\n    }\n\n    /** See {@link dev.ebullient.convert.tools.dnd5e.qute.AcHp#ac} */\n    public Integer getAc() {\n        return acHp.ac;\n    }\n\n    /** See {@link dev.ebullient.convert.tools.dnd5e.qute.AcHp#acText} */\n    public String getAcText() {\n        return acHp.acText;\n    }\n\n    /** See {@link dev.ebullient.convert.tools.dnd5e.qute.AcHp#hpText} */\n    public String getHpText() {\n        return acHp.hpText;\n    }\n\n    /** See {@link dev.ebullient.convert.tools.dnd5e.qute.AcHp#hitDice} */\n    public String getHitDice() {\n        return acHp.hitDice;\n    }\n\n    /** See {@link dev.ebullient.convert.tools.dnd5e.qute.ImmuneResist#vulnerable} */\n    public String getVulnerable() {\n        return immuneResist.vulnerable;\n    }\n\n    /** See {@link dev.ebullient.convert.tools.dnd5e.qute.ImmuneResist#resist} */\n    public String getResist() {\n        return immuneResist.resist;\n    }\n\n    /** See {@link dev.ebullient.convert.tools.dnd5e.qute.ImmuneResist#immune} */\n    public String getImmune() {\n        return immuneResist.immune;\n    }\n\n    /** See {@link dev.ebullient.convert.tools.dnd5e.qute.ImmuneResist#conditionImmune} */\n    public String getConditionImmune() {\n        return immuneResist.conditionImmune;\n    }\n\n    /**\n     * A minimal YAML snippet containing object attributes required by the\n     * Initiative Tracker plugin. Use this in frontmatter.\n     */\n    public String get5eInitiativeYaml() {\n        Map<String, Object> map = new LinkedHashMap<>();\n        addUnlessEmpty(map, \"name\", name);\n        addIntegerUnlessEmpty(map, \"ac\", acHp.ac);\n        addIntegerUnlessEmpty(map, \"hp\", acHp.hp);\n        map.put(\"stats\", scores.toArray()); // for initiative\n        addUnlessEmpty(map, \"source\", getBooks());\n        return Tui.plainYaml().dump(map).trim();\n    }\n\n    /**\n     * Complete object attributes in the format required by the Fantasy statblock plugin.\n     * Uses double-quoted syntax to deal with a variety of characters occuring in\n     * trait descriptions. Usable in frontmatter or Fantasy Statblock code blocks.\n     */\n    public String get5eStatblockYaml() {\n        Map<String, Object> map = new LinkedHashMap<>();\n        addUnlessEmpty(map, \"name\", name);\n        addUnlessEmpty(map, \"size\", size);\n        addUnlessEmpty(map, \"type\", creatureType);\n\n        addIntegerUnlessEmpty(map, \"ac\", acHp.ac);\n        addIntegerUnlessEmpty(map, \"hp\", acHp.hp);\n\n        map.put(\"stats\", scores.toArray());\n        addUnlessEmpty(map, \"speed\", speed);\n\n        addUnlessEmpty(map, \"damage_vulnerabilities\", immuneResist.vulnerable);\n        addUnlessEmpty(map, \"damage_resistances\", immuneResist.resist);\n        addUnlessEmpty(map, \"damage_immunities\", immuneResist.immune);\n        addUnlessEmpty(map, \"condition_immunities\", immuneResist.conditionImmune);\n        addUnlessEmpty(map, \"senses\", senses);\n\n        addUnlessEmpty(map, \"actions\", action);\n        addUnlessEmpty(map, \"source\", getBooks());\n        if (token != null) {\n            map.put(\"image\", token.getVaultPath());\n        }\n\n        // De-markdown-ify\n        return Tui.quotedYaml().dump(map).trim()\n                .replaceAll(\"`\", \"\")\n                .replaceAll(\"\\\\*([^*]+)\\\\*\", \"$1\") // em\n                .replaceAll(\"\\\\*([^*]+)\\\\*\", \"$1\") // bold\n                .replaceAll(\"\\\\*([^*]+)\\\\*\", \"$1\"); // bold em\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QutePsionic.java",
    "content": "package dev.ebullient.convert.tools.dnd5e.qute;\n\nimport java.util.Collection;\nimport java.util.List;\n\nimport dev.ebullient.convert.qute.NamedText;\nimport dev.ebullient.convert.tools.CompendiumSources;\nimport dev.ebullient.convert.tools.Tags;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * 5eTools psionic talent attributes ({@code psionic2md.txt})\n *\n * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}.\n */\n@TemplateData\npublic class QutePsionic extends Tools5eQuteBase {\n\n    /** Psionic type and order (string) */\n    public final String typeOrder;\n    /** Psionic focus (string) */\n    public final String focus;\n    /** Psionic mode as list of {@link dev.ebullient.convert.qute.NamedText} */\n    public final Collection<NamedText> modes;\n\n    public QutePsionic(CompendiumSources sources, String name, String source,\n            String typeOrder, String focus, Collection<NamedText> modes,\n            String text, Tags tags) {\n        super(sources, name, source, List.of(), text, tags);\n        this.typeOrder = typeOrder;\n        this.focus = focus;\n        this.modes = modes;\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteRace.java",
    "content": "package dev.ebullient.convert.tools.dnd5e.qute;\n\nimport java.util.List;\n\nimport dev.ebullient.convert.config.TtrpgConfig;\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.Tools5eSources;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * 5eTools race attributes ({@code race2md.txt})\n *\n * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}.\n */\n@TemplateData\npublic class QuteRace extends Tools5eQuteBase {\n\n    /** Ability scores associated with this race (comma-separated list of scores or choices) */\n    public final String ability;\n    /** type of race or subrace (humanoid, ooze, undead, etc.) */\n    public final String type;\n    /** Size: Small or Medium */\n    public final String size;\n    /** Speed: 30 ft. May include additional values, like flight or swim speed. */\n    public final String speed;\n    /** Spellcasting ability score */\n    public final String spellcasting;\n    /** Formatted text with subsections describing racial traits */\n    public final String traits;\n    /** Formatted text describing the race. Optional. Same as {resource.text} */\n    public final String description;\n\n    public QuteRace(Tools5eSources sources, String name, String source,\n            String ability, String type, String size, String speed,\n            String spellcasting, String traits, String description,\n            List<ImageRef> images, Tags tags) {\n        super(sources, name, source, images, description, tags);\n        this.ability = ability;\n        this.type = type;\n        this.size = size;\n        this.speed = speed;\n        this.spellcasting = spellcasting;\n        this.traits = traits;\n        this.description = description;\n    }\n\n    /** CSS class for this resource: {@code json5e-race} or {@code json5e-species} */\n    public String getCssClass() {\n        return TtrpgConfig.getConfig().racesAsSpecies() ? \"json5e-species\" : \"json5e-race\";\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteReward.java",
    "content": "package dev.ebullient.convert.tools.dnd5e.qute;\n\nimport java.util.List;\n\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.tools.CompendiumSources;\nimport dev.ebullient.convert.tools.Tags;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * 5eTools reward attributes ({@code reward2md.txt})\n *\n * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}.\n */\n@TemplateData\npublic class QuteReward extends Tools5eQuteBase {\n\n    /**\n     * Description of special ability granted by this reward, if defined separately. This is usually included in reward text.\n     */\n    public final String ability;\n    /** Reward detail string (similar to item detail). May include the reward type and rarity if either are defined. */\n    public final String detail;\n    /** Formatted text describing sigature spells. Not commonly used. */\n    public final String signatureSpells;\n\n    public QuteReward(CompendiumSources sources, String name, String source,\n            String ability, String detail, String signatureSpells,\n            List<ImageRef> images, String text, Tags tags) {\n        super(sources, name, source, images, text, tags);\n        withTemplate(\"reward2md.txt\");\n        this.ability = ability;\n        this.detail = detail;\n        this.signatureSpells = signatureSpells;\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteSpell.java",
    "content": "package dev.ebullient.convert.tools.dnd5e.qute;\n\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.Tools5eSources;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * 5eTools spell attributes ({@code spell2md.txt})\n *\n * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}.\n */\n@TemplateData\npublic class QuteSpell extends Tools5eQuteBase {\n\n    /** Spell level */\n    public final String level;\n    /** Spell school */\n    public final String school;\n    /** true for ritual spells */\n    public final boolean ritual;\n    /** Formatted: casting time */\n    public final String time;\n    /** Formatted: spell range */\n    public final String range;\n    /** Formatted: spell components */\n    public final String components;\n    /** Formatted: spell range */\n    public final String duration;\n    /** Formatted: Ability checks */\n    public final String abilityChecks;\n    /** Formatted: Creature types */\n    public final String affectsCreatureTypes;\n    /** Formatted/mapped: Areas */\n    public final String areaTags;\n    /** Formatted: Condition immunities */\n    public final String conditionImmune;\n    /** Formatted: Conditions */\n    public final String conditionInflict;\n    /** Formatted: Damage immunities */\n    public final String damageImmune;\n    /** Formatted: Damage types */\n    public final String damageInflict;\n    /** Formatted: Damage resistances */\n    public final String damageResist;\n    /** Formatted: Damage vulnerabilities */\n    public final String damageVulnerable;\n    /** Formatted/mapped: Misc tags */\n    public final String miscTags;\n    /** Formatted: Saving throws */\n    public final String savingThrows;\n    /** Formatted: Scaling damage dice entries */\n    public final String scalingLevelDice;\n    /** Formatted: Spell attack forms */\n    public final String spellAttacks;\n    /** At higher levels text */\n    public final String higherLevels;\n    /** String: rendered list of links to classes that grant access to this spell. May be incomplete or empty. */\n    public final String backgrounds;\n    /** String: rendered list of links to classes that can use this spell. May be incomplete or empty. */\n    public final String classes;\n    /** String: rendered list of links to feats that grant acccess to this spell. May be incomplete or empty. */\n    public final String feats;\n    /** String: rendered list of links to optional features that grant access to this spell. May be incomplete or empty. */\n    public final String optionalfeatures;\n    /** String: rendered list of links to races that can use this spell. May be incomplete or empty. */\n    public final String races;\n    /** List of links to resources (classes, subclasses, feats, etc.) that have access to this spell */\n    public final Collection<String> references;\n\n    public QuteSpell(Tools5eSources sources, String name, String source, String level,\n            String school, boolean ritual, String time, String range,\n            String components, String duration,\n            String abilityChecks, String affectsCreatureTypes,\n            String areaTags, String conditionImmune,\n            String conditionInflict, String damageImmune,\n            String damageInflict, String damageResist,\n            String damageVulnerable, String miscTags,\n            String savingThrows, String scalingLevelDice,\n            String spellAttacks,\n            String higherLevels, Collection<String> references, List<ImageRef> images, String text, Tags tags) {\n        super(sources, name, source, images, text, tags);\n\n        this.level = level;\n        this.school = school;\n        this.ritual = ritual;\n        this.time = time;\n        this.range = range;\n        this.components = components;\n        this.duration = duration;\n        this.abilityChecks = abilityChecks;\n        this.affectsCreatureTypes = affectsCreatureTypes;\n        this.areaTags = areaTags;\n        this.conditionImmune = conditionImmune;\n        this.conditionInflict = conditionInflict;\n        this.damageImmune = damageImmune;\n        this.damageInflict = damageInflict;\n        this.damageResist = damageResist;\n        this.damageVulnerable = damageVulnerable;\n        this.miscTags = miscTags;\n        this.savingThrows = savingThrows;\n        this.scalingLevelDice = scalingLevelDice;\n        this.spellAttacks = spellAttacks;\n        this.higherLevels = higherLevels == null || higherLevels.isBlank() ? null : higherLevels;\n        this.references = references;\n        this.backgrounds = references.stream()\n                .filter(s -> s.contains(\"background\"))\n                .distinct()\n                .sorted()\n                .collect(Collectors.joining(\"; \"));\n        this.classes = references.stream()\n                .filter(s -> s.contains(\"class\"))\n                .distinct()\n                .sorted()\n                .collect(Collectors.joining(\"; \"));\n        this.feats = references.stream()\n                .filter(s -> s.contains(\"feat\"))\n                .filter(s -> !s.contains(\"optional-feature\"))\n                .distinct()\n                .sorted()\n                .collect(Collectors.joining(\"; \"));\n        this.optionalfeatures = references.stream()\n                .filter(s -> s.contains(\"optional-feature\"))\n                .distinct()\n                .sorted()\n                .collect(Collectors.joining(\"; \"));\n        this.races = references.stream()\n                .filter(s -> s.contains(\"race\"))\n                .distinct()\n                .sorted()\n                .collect(Collectors.joining(\"; \"));\n    }\n\n    /** List of class names (not links) that can use this spell. */\n    public List<String> getClassList() {\n        return references == null || references.isEmpty()\n                ? List.of()\n                : references.stream()\n                        .filter(s -> s.contains(\"class\"))\n                        .map(s -> s.replaceAll(\"\\\\[(.*?)\\\\].*\", \"$1\"))\n                        .distinct()\n                        .sorted()\n                        .toList();\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteSubclass.java",
    "content": "package dev.ebullient.convert.tools.dnd5e.qute;\n\nimport java.util.List;\n\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.Tools5eSources;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * 5eTools subclass attributes ({@code subclass2md.txt})\n *\n * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}.\n */\n@TemplateData\npublic class QuteSubclass extends Tools5eQuteBase {\n    /** Name of the parent class */\n    public final String parentClass;\n    /** Markdown link to the parent class */\n    public final String parentClassLink;\n    /** Source of the parent class (abbreviation) */\n    public final String parentClassSource;\n    /** Title of subclass: \"Bard College\", or \"Primal Path\" */\n    public final String subclassTitle;\n    /** A pre-foramatted markdown callout describing subclass spell or feature progression */\n    public final String classProgression;\n\n    public QuteSubclass(Tools5eSources sources,\n            String name, String source,\n            String parentClass,\n            String parentClassLink,\n            String parentClassSource,\n            String subclassTitle,\n            String classProgression,\n            String text, List<ImageRef> images, Tags tags) {\n        super(sources, name, source, images, text, tags);\n        this.parentClass = parentClass;\n        this.parentClassLink = parentClassLink;\n        this.parentClassSource = parentClassSource;\n        this.subclassTitle = subclassTitle;\n        this.classProgression = classProgression;\n    }\n\n    @Override\n    public String targetFile() {\n        return linkifier().getSubclassResource(name,\n                parentClass, parentClassSource,\n                sources.primarySource());\n    }\n\n    @Override\n    public String title() {\n        return parentClass + \": \" + getName();\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/qute/QuteVehicle.java",
    "content": "package dev.ebullient.convert.tools.dnd5e.qute;\n\nimport static dev.ebullient.convert.StringUtil.pluralize;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\n\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.qute.NamedText;\nimport dev.ebullient.convert.qute.QuteUtil;\nimport dev.ebullient.convert.tools.CompendiumSources;\nimport dev.ebullient.convert.tools.Tags;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * 5eTools vehicle attributes ({@code vehicle2md.txt})\n *\n * Several different types of vehicle use this template, including:\n * Ship, spelljammer, infernal war manchie, objects and creatures.\n * They can have very different properties. Treat most as optional.\n *\n * Extension of {@link dev.ebullient.convert.tools.dnd5e.qute.Tools5eQuteBase}.\n */\n@TemplateData\npublic class QuteVehicle extends Tools5eQuteBase {\n\n    /** Vehicle type: Ship, Spelljammer, Infernal War Machine, Creature, Object */\n    public final String vehicleType;\n    /** Ship size and dimensions. Used by Ship, Infernal War Machine */\n    public final String sizeDimension;\n    /** Vehicle terrain as a comma-separated list (all) */\n    public final String terrain;\n    /**\n     * Object ability scores as {@link dev.ebullient.convert.tools.dnd5e.qute.AbilityScores}\n     * Used by Ship, Infernal War Machine, Creature, Object\n     */\n    public final AbilityScores scores;\n\n    /** Vehicle immunities and resistances as {@link dev.ebullient.convert.tools.dnd5e.qute.ImmuneResist} */\n    public final ImmuneResist immuneResist;\n\n    /**\n     * Ship capacity and pace attributes as {@link dev.ebullient.convert.tools.dnd5e.qute.QuteVehicle.ShipCrewCargoPace}.\n     */\n    public final ShipCrewCargoPace shipCrewCargoPace;\n    /** List of vehicle actions as a collection of {@link dev.ebullient.convert.qute.NamedText} */\n    public final Collection<NamedText> action;\n    /**\n     * Ship sections and traits as {@link dev.ebullient.convert.tools.dnd5e.qute.QuteVehicle.ShipAcHp} (hull, sails,\n     * oars, .. )\n     */\n    public final List<ShipSection> shipSections;\n\n    /** Token image as {@link dev.ebullient.convert.qute.ImageRef} */\n    public final ImageRef token;\n\n    public QuteVehicle(CompendiumSources sources, String name, String source,\n            String vehicleType, String terrain,\n            AbilityScores scores, String size,\n            ImmuneResist immuneResist,\n            ShipCrewCargoPace shipCrewCargoPace,\n            List<ShipSection> shipSections,\n            Collection<NamedText> action,\n            ImageRef token, List<ImageRef> images, String text, Tags tags) {\n        super(sources, name, source, images, text, tags);\n\n        this.vehicleType = vehicleType;\n        this.terrain = terrain;\n\n        this.scores = scores;\n        this.sizeDimension = size;\n\n        this.immuneResist = immuneResist;\n\n        this.shipCrewCargoPace = shipCrewCargoPace;\n        this.shipSections = shipSections;\n        this.action = action;\n\n        this.token = token;\n    }\n\n    /** True if this vehicle is a Ship */\n    public boolean getIsShip() {\n        return \"SHIP\".equals(vehicleType);\n    }\n\n    /** True if this vehicle is a Spelljammer */\n    public boolean getIsSpelljammer() {\n        return \"SPELLJAMMER\".equals(vehicleType);\n    }\n\n    /** True if this vehicle is an Infernal War Machine */\n    public boolean getIsWarMachine() {\n        return \"INFWAR\".equals(vehicleType);\n    }\n\n    /** True if this vehicle is a Creature */\n    public boolean getIsCreature() {\n        return \"CREATURE\".equals(vehicleType);\n    }\n\n    /** True if this vehicle is an Object */\n    public boolean getIsObject() {\n        return \"OBJECT\".equals(vehicleType);\n    }\n\n    /**\n     * 5eTools Ship crew, cargo, and pace attributes\n     *\n     * This data object provides a default mechanism for creating\n     * a marked up string based on the attributes that are present.\n     *\n     * To use it, reference it directly:\n     *\n     * ```md\n     * {#if resource.shipCrewCargoPace}\n     * {resource.shipCrewCargoPace}\n     * {/if}\n     * ```\n     */\n    @TemplateData\n    public static class ShipCrewCargoPace implements QuteUtil {\n        final String vehicleType;\n        /** Crew capacity (number) */\n        public String crew;\n        /** Additional crew notes */\n        public String crewText;\n        /** Passenger capacity (number) */\n        public String passenger;\n        /** Cargo capacity (string) */\n        public String cargo;\n        /** Spelljammer or Infernal War Machine HP/AC */\n        public ShipAcHp acHp;\n        /** Spelljammer Keel/Beam */\n        public String keelBeam;\n        /**\n         * Ship pace (number, mph)\n         * Ship speed is pace * 10 (*Special Travel Pace*, DMG p242).\n         */\n        public Integer shipPace;\n        /**\n         * Spelljammer speed and pace (preformatted string)\n         */\n        public String speedPace;\n\n        public ShipCrewCargoPace(String vehicleType, String crew, String crewText, String passenger,\n                String cargo, Integer pace, String speedPace, ShipAcHp acHp,\n                String keelBeam) {\n            this.vehicleType = vehicleType;\n            this.crew = crew;\n            this.crewText = crewText;\n            this.passenger = passenger;\n            this.cargo = cargo;\n            this.shipPace = pace;\n            this.speedPace = speedPace;\n            this.acHp = acHp;\n            this.keelBeam = keelBeam;\n        }\n\n        public String toString() {\n            List<String> out = new ArrayList<>();\n            String crewPresent = isPresent(crew)\n                    ? crew + (isPresent(crewText) ? \" \" + crewText : \"\")\n                    : \"\\u2014\";\n\n            if (\"SPELLJAMMER\".equals(vehicleType)) {\n                // This is a spelljammer\n                String acText = isPresent(acHp.acText) ? \" (\" + acHp.acText + \")\" : \"\";\n                out.add(\"- **Armor Class** \" + (isPresent(acHp.ac) ? (acHp.ac + acText) : \"\\u2014\"));\n                out.add(\"- **Hit Points** \" + (isPresent(acHp.hp) ? acHp.hp : \"\\u2014\"));\n                out.add(\"- **Damage Threshold** \" + (isPresent(acHp.dt) ? acHp.dt : \"\\u2014\"));\n                out.add(\"- **Speed** \" + speedPace);\n                out.add(\"- **Cargo** \" + (isPresent(cargo) ? cargo + pluralize(\" ton\", cargo.equals(\"1\") ? 1 : 2) : \"\\u2014\"));\n                out.add(\"- **Crew** \" + crewPresent);\n                out.add(\"- **Keel/Beam** \" + (isPresent(keelBeam) ? keelBeam : \"\\u2014\"));\n                out.add(\"- **Cost** \" + (isPresent(acHp.cost) ? acHp.cost : \"\\u2014\"));\n            } else {\n                if (isPresent(crew) || isPresent(passenger)) {\n                    List<String> inner = new ArrayList<>();\n                    if (isPresent(crew)) {\n                        inner.add(crewPresent);\n                    }\n                    if (isPresent(passenger)) {\n                        inner.add(passenger + pluralize(\" passenger\", passenger.equals(\"1\") ? 1 : 2));\n                    }\n                    out.add(\"- **Creature Capacity** \" + String.join(\", \", inner));\n                }\n                if (isPresent(cargo)) {\n                    out.add(\"- **Cargo Capacity** \" + cargo);\n                }\n                if (isPresent(acHp)) {\n                    out.add(acHp.toString());\n                }\n                if (\"SHIP\".equals(vehicleType) && isPresent(shipPace)) {\n                    out.add(\"- **Travel Pace** \"\n                            + shipPace + \" miles per hour (\"\n                            + (shipPace * 24) + \" miles per day)\");\n                    out.add(\"- *Speed* \" + (shipPace * 10) + \" ft. ^[Based on _Special Travel Pace_, DMG p242]\");\n                } else if (isPresent(speedPace)) {\n                    if (\"INFWAR\".equals(vehicleType)) {\n                        int s = Integer.parseInt(speedPace.replace(\" ft.\", \"\"));\n                        int pace = s / 10;\n                        int day = (s * 24) / 10;\n                        out.add(\"- **Speed** \" + s + \" ft.\\n\"\n                                + \"- *Travel Pace* \"\n                                + pace + \" miles per hour (\"\n                                + day + \" miles per day) ^[Based on _Special Travel Pace_, DMG p242]\");\n                    } else {\n                        out.add(\"- **Speed** \" + speedPace);\n                    }\n                }\n            }\n\n            return String.join(\"\\n\", out);\n        }\n    }\n\n    /**\n     * 5eTools vehicle armor class and hit points attributes\n     *\n     * This data object provides a default mechanism for creating\n     * a marked up string based on the attributes that are present.\n     * To use it, reference it directly.\n     *\n     */\n    @TemplateData\n    public static class ShipAcHp extends AcHp {\n        final String vehicleType;\n\n        /** Damage threshold; number */\n        public Integer dt;\n        /** Infernal War Machine mishap threshold; number */\n        public Integer mt;\n        /** Cost (per unit); preformatted string */\n        public String cost;\n\n        public ShipAcHp(String type, Integer ac, String acText, Integer hp, String hpText, Integer dt, Integer mt,\n                String cost) {\n            super(ac, acText, hp, hpText, null);\n            this.vehicleType = type;\n            this.dt = dt;\n            this.mt = mt;\n            this.cost = cost;\n        }\n\n        public ShipAcHp(String type, AcHp creatureAcHp) {\n            super(creatureAcHp);\n            this.vehicleType = type;\n        }\n\n        public String toString() {\n            if (\"CREATURE\".equals(vehicleType)) {\n                return super.toString();\n            }\n            List<String> out = new ArrayList<>();\n            if (isPresent(ac)) {\n                out.add(\"- **Armor Class** \" + ac + (isPresent(acText) ? \" (\" + acText + \")\" : \"\"));\n            }\n            if (isPresent(hp)) {\n                List<String> threshold = new ArrayList<>();\n                if (isPresent(dt)) {\n                    threshold.add(\"damage threshold \" + dt);\n                }\n                if (isPresent(mt)) {\n                    threshold.add(\"mishap threshold \" + mt);\n                }\n                String thresholdText = String.join(\", \", threshold);\n\n                out.add(\"- **Hit Points** \" + hp\n                        + (isPresent(thresholdText) ? \" (\" + thresholdText + \")\" : \"\")\n                        + (hpText != null ? \"; \" + hpText : \"\"));\n            }\n            if (isPresent(cost)) {\n                out.add(\"- **Cost** \" + cost);\n            }\n            return String.join(\"\\n\", out);\n        }\n    }\n\n    /**\n     * 5eTools vehicle sections\n     *\n     * This data object provides a default mechanism for creating\n     * a marked up string based on the attributes that are present.\n     * To use it, reference it directly.\n     *\n     */\n    @TemplateData\n    public static class ShipSection implements QuteUtil {\n        /** Name */\n        public String name;\n        /** Armor class and hit points as {@link dev.ebullient.convert.tools.dnd5e.qute.QuteVehicle.ShipAcHp} */\n        public ShipAcHp acHp;\n        /** Speed as a list of pre-formatted strings */\n        public List<String> speed;\n        /** Pre-formatted text description */\n        public List<String> desc;\n        /** Pre-formatted actions */\n        public List<String> actions;\n\n        public ShipSection(String name, ShipAcHp acHp, List<String> speed, List<String> desc, List<String> actions) {\n            this.name = name;\n            this.acHp = acHp;\n            this.speed = speed;\n            this.desc = desc;\n            this.actions = actions;\n        }\n\n        public String toString() {\n            List<String> out = new ArrayList<>();\n\n            out.add(\"## \" + name);\n            out.add(\"\");\n            if (isPresent(acHp)) {\n                out.add(acHp.toString());\n            }\n            if (isPresent(speed)) {\n                speed.forEach(s -> out.add(\"- \" + s));\n            }\n            if (isPresent(desc)) {\n                desc.forEach(d -> {\n                    out.add(\"\");\n                    out.add(d);\n                });\n            }\n            if (isPresent(actions)) {\n                actions.forEach(a -> {\n                    out.add(\"\");\n                    out.add(a);\n                });\n            }\n            return String.join(\"\\n\", out)\n                    .replaceAll(\"\\n{3,}\", \"\\n\\n\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/qute/Tools5eQuteBase.java",
    "content": "package dev.ebullient.convert.tools.dnd5e.qute;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\n\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.qute.QuteBase;\nimport dev.ebullient.convert.tools.CompendiumSources;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.Tools5eLinkifier;\nimport dev.ebullient.convert.tools.dnd5e.Tools5eSources;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * Attributes for notes that are generated from the 5eTools data.\n * This is a trivial extension of {@link dev.ebullient.convert.qute.QuteBase}.\n *\n * Notes created from {@code Tools5eQuteBase} will use a specific template\n * for the type. For example, {@code QuteBackground} will use {@code background2md.txt}.\n */\n@TemplateData\npublic class Tools5eQuteBase extends QuteBase {\n\n    /** List of images as {@link dev.ebullient.convert.qute.ImageRef} (optional) */\n    public final List<ImageRef> fluffImages;\n\n    String targetPath;\n    String filename;\n    String template;\n\n    public Tools5eQuteBase(CompendiumSources sources, String name, String source, List<ImageRef> fluffImages, String text,\n            Tags tags) {\n        super(sources, name, source, text, tags);\n        this.fluffImages = isPresent(fluffImages) ? fluffImages : List.of();\n    }\n\n    @Override\n    public Tools5eSources sources() {\n        return (Tools5eSources) sources;\n    }\n\n    /**\n     * Return true if any images are present\n     */\n    public boolean getHasImages() {\n        return !fluffImages.isEmpty();\n    }\n\n    /**\n     * Return true if more than one image is present\n     */\n    public boolean getHasMoreImages() {\n        return fluffImages.size() > 1;\n    }\n\n    /**\n     * Return an embedded wikilink to the first image\n     * Will have the \"right\" anchor tag.\n     */\n    public String getShowPortraitImage() {\n        if (fluffImages.isEmpty()) {\n            return \"\";\n        }\n        return fluffImages.get(0).getEmbeddedLink(\"right\");\n    }\n\n    /**\n     * Return embedded wikilinks for all images\n     * If there is more than one, they will be displayed in a gallery.\n     */\n    public String getShowAllImages() {\n        return createImageLinks(false);\n    }\n\n    /**\n     * Return embedded wikilinks for all but the first image\n     * If there is more than one, they will be displayed in a gallery.\n     */\n    public String getShowMoreImages() {\n        return createImageLinks(true);\n    }\n\n    private String createImageLinks(boolean omitFirst) {\n        if (fluffImages.isEmpty()) {\n            return \"\";\n        }\n        if (fluffImages.size() == 1 && !omitFirst) {\n            return fluffImages.get(0).getEmbeddedLink(\"center\");\n        }\n        if (fluffImages.size() == 2 && omitFirst) {\n            return fluffImages.get(1).getEmbeddedLink(\"center\");\n        }\n        List<String> lines = new ArrayList<>();\n        lines.add(\"> [!gallery]\");\n        for (int i = omitFirst ? 1 : 0; i < fluffImages.size(); i++) {\n            lines.add(fluffImages.get(i).getEmbeddedLink(\"\")); // no anchor\n        }\n        return String.join(\"\\n\", lines);\n    }\n\n    public Tools5eQuteBase withTargetFile(String filename) {\n        this.filename = filename;\n        return this;\n    }\n\n    public String targetFile() {\n        if (filename != null) {\n            return filename;\n        }\n        return linkifier().getTargetFileName(getName(), sources());\n    }\n\n    public Tools5eQuteBase withTargetPath(String path) {\n        this.targetPath = path;\n        return this;\n    }\n\n    public String targetPath() {\n        if (targetPath != null) {\n            return targetPath;\n        }\n        if (sources() != null) {\n            return linkifier().getRelativePath(sources());\n        }\n        return \".\";\n    }\n\n    public Tools5eQuteBase withTemplate(String template) {\n        this.template = template;\n        return this;\n    }\n\n    public String template() {\n        return template == null ? super.template() : template;\n    }\n\n    public Collection<QuteBase> inlineNotes() {\n        return sources() == null\n                ? List.of()\n                : Tools5eSources.getInlineNotes(sources().getKey());\n    }\n\n    @Override\n    public String getVaultPath() {\n        String file = targetFile();\n        if (!file.endsWith(\".md\")) {\n            file += \".md\";\n        }\n        return linkifier().vaultRoot(sources()) + targetPath() + \"/\" + file;\n    }\n\n    protected Tools5eLinkifier linkifier() {\n        return Tools5eLinkifier.instance();\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/qute/Tools5eQuteNote.java",
    "content": "package dev.ebullient.convert.tools.dnd5e.qute;\n\nimport java.util.Collection;\nimport java.util.List;\n\nimport dev.ebullient.convert.qute.QuteBase;\nimport dev.ebullient.convert.qute.QuteNote;\nimport dev.ebullient.convert.tools.CompendiumSources;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.dnd5e.Tools5eSources;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * Attributes for notes that are generated from the 5eTools data.\n * This is a trivial extension of {@link dev.ebullient.convert.qute.QuteNote}.\n *\n * Notes created from {@code Tools5eQuteNote} will use the {@code note2md.txt} template.\n */\n@TemplateData\npublic class Tools5eQuteNote extends QuteNote {\n\n    String targetPath;\n    String filename;\n    String template;\n\n    public Tools5eQuteNote(CompendiumSources sources, String name, String sourceText, String text, Tags tags) {\n        super(sources, name, sourceText, text, tags);\n    }\n\n    public Tools5eQuteNote(String name, String sourceText, String text, Tags tags) {\n        super(name, sourceText, text, tags);\n    }\n\n    public Tools5eQuteNote(String name, String sourceText, List<String> text, Tags tags) {\n        super(name, sourceText, text, tags);\n    }\n\n    public Tools5eQuteNote withTargetFile(String filename) {\n        this.filename = filename;\n        return this;\n    }\n\n    public String targetFile() {\n        return filename == null ? super.targetFile() : filename;\n    }\n\n    public Tools5eQuteNote withTargetPath(String path) {\n        this.targetPath = path;\n        return this;\n    }\n\n    public String targetPath() {\n        return targetPath == null ? super.targetPath() : targetPath;\n    }\n\n    public Tools5eQuteNote withTemplate(String template) {\n        this.template = template;\n        return this;\n    }\n\n    public String template() {\n        return template == null ? super.template() : template;\n    }\n\n    public Collection<QuteBase> inlineNotes() {\n        return sources() == null\n                ? List.of()\n                : Tools5eSources.getInlineNotes(sources().getKey());\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/dnd5e/qute/package-info.java",
    "content": "/**\n * <h1>5eTools templates</h1>\n *\n * Qute templates for generating content from 5eTools data.\n */\npackage dev.ebullient.convert.tools.dnd5e.qute;\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteAbility.java",
    "content": "package dev.ebullient.convert.tools.pf2e;\n\nimport static dev.ebullient.convert.StringUtil.join;\nimport static dev.ebullient.convert.StringUtil.parenthesize;\n\nimport java.util.Set;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteAbility;\n\npublic class Json2QuteAbility extends Json2QuteBase {\n\n    public Json2QuteAbility(Pf2eIndex index, Pf2eIndexType type, JsonNode rootNode) {\n        super(index, type, rootNode);\n    }\n\n    @Override\n    protected QuteAbility buildQuteNote() {\n        return Pf2eAbility.createAbility(rootNode, this, sources);\n    }\n\n    public enum Pf2eAbility implements Pf2eJsonNodeReader {\n        /** e.g. {@code \"Dirty Bomb\"} */\n        name,\n        /** @see Pf2eActivity */\n        activity,\n        /**\n         * Activation components. The actual data is much more freeform than the schema indicates, e.g.\n         * {@code [\"command\", \"{@action Recall Knowledge}\", \"1 minute\", \"{@action Interact} 1 minute\"]}.\n         */\n        components,\n        /** e.g. {@code \"one batch of infused reagents.\"} */\n        cost,\n        /** String array of creatures/archetypes this ability is associated with, e.g. {@code [\"Skeleton\"]} */\n        creature, // TODO\n        /** @see dev.ebullient.convert.tools.pf2e.Pf2eJsonNodeReader.Pf2eFrequency */\n        frequency,\n        /**\n         * If present this is a generic ability, defined further in a standalone note.\n         *\n         * @see Pf2eGenericAbilityReference\n         */\n        generic,\n        /** e.g. {@code \"The corpselight is in wisp form and is adjacent to a Medium corpse\"} */\n        prerequisites,\n        /** How to display this block, {@code \"compact\"} or {@code \"full\"} */\n        style, // TODO\n        /** e.g. {@code \"the activation takes {@as 2} if the spell normally takes {@as 1} to cast\"} */\n        note,\n        /** @see dev.ebullient.convert.tools.pf2e.Pf2eJsonNodeReader.Pf2eNumberUnitEntry */\n        range,\n        /** e.g. {@code \"Your last action reduced an enemy to 0 Hit Points} */\n        requirements,\n        /** e.g. {@code \"You strike a foe with the blast lance\"} */\n        trigger,\n        /**\n         * Listed in the schema as an array of entries, but actual data only ever has a single string in an array.\n         * e.g. {@code [\"Multiple critical failures might cause the contact to work against the PCs in some way\"]}\n         */\n        special,\n        /** Nestable entries for the ability effect. */\n        entries;\n\n        private static QuteAbility createAbility(JsonNode node, JsonSource convert, Pf2eSources sources) {\n            Tags tags = new Tags();\n            Set<String> traits = convert.collectTraitsFrom(node, tags);\n\n            return new QuteAbility(sources,\n                    name.getTextFrom(node).map(convert::replaceText).orElse(\"Activate\"),\n                    generic.getLinkFrom(node, convert),\n                    entries.transformTextFrom(node, \"\\n\", convert),\n                    tags,\n                    traits,\n                    activity.getActivityFrom(node, convert),\n                    range.getRangeFrom(node, convert),\n                    components.getActivationComponentsFrom(node, traits, convert),\n                    requirements.replaceTextFrom(node, convert),\n                    prerequisites.replaceTextFrom(node, convert),\n                    cost.replaceTextFrom(node, convert)\n                            // remove trailing period\n                            .replaceFirst(\"^(.*)\\\\.$\", \"\\1\"),\n                    trigger.replaceTextFrom(node, convert),\n                    frequency.getFrequencyFrom(node, convert),\n                    special.transformTextFrom(node, \"\\n\", convert),\n                    note.replaceTextFrom(node, convert),\n                    sources == null, convert);\n        }\n\n        public static QuteAbility createEmbeddedAbility(JsonNode node, JsonSource convert) {\n            return createAbility(node, convert, null);\n        }\n\n        public String getLinkFrom(JsonNode node, JsonSource convert) {\n            return getObjectFrom(node)\n                    .map(n -> convert.linkify(\n                            Pf2eIndexType.fromText(Pf2eGenericAbilityReference.tag.getTextOrNull(n)),\n                            join(\"|\",\n                                    Pf2eGenericAbilityReference.name.getTextFrom(n)\n                                            .or(() -> name.getTextFrom(node))\n                                            // Add the hash as a parenthesized string after the name\n                                            .map(s -> s + Pf2eGenericAbilityReference.add_hash.getTextFrom(n)\n                                                    .map(hash -> \" \" + parenthesize(hash)).orElse(\"\"))\n                                            .orElse(null),\n                                    Pf2eGenericAbilityReference.source.getTextOrNull(n))))\n                    .orElse(null);\n        }\n\n        private enum Pf2eGenericAbilityReference implements Pf2eJsonNodeReader {\n            /** An extra string to add to an ability for finding its link, e.g. {@code \"Rogue\"} */\n            add_hash,\n            /** e.g. {@code \"Curse of the Werecreature\"}, or {@code \"Nimble Dodge (Rogue)\"} */\n            name,\n            /** The type of the resource, e.g. {@code \"ability\"} or {@code \"feat\"} */\n            tag,\n            /** The source, e.g. {@code \"B1\"} */\n            source;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteAction.java",
    "content": "package dev.ebullient.convert.tools.pf2e;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteAction;\nimport io.quarkus.runtime.annotations.RegisterForReflection;\n\npublic class Json2QuteAction extends Json2QuteBase {\n\n    public Json2QuteAction(Pf2eIndex index, JsonNode node) {\n        super(index, Pf2eIndexType.action, node);\n    }\n\n    @Override\n    protected QuteAction buildQuteResource() {\n        Tags tags = new Tags(sources);\n        List<String> text = new ArrayList<>();\n\n        appendToText(text, SourceField.entries.getFrom(rootNode), \"##\");\n        appendToText(text, Pf2eAction.info.getFrom(rootNode), null);\n\n        ActionType actionType = Pf2eAction.actionType.fieldFromTo(rootNode, ActionType.class, tui());\n\n        if (actionType == null) {\n            tags.add(\"action\");\n        } else {\n            actionType.addTags(this, tags);\n        }\n\n        return new QuteAction(\n                getSources(), text, tags,\n                Pf2eAction.cost.transformTextFrom(rootNode, \", \", this),\n                Pf2eAction.trigger.transformTextFrom(rootNode, \", \", this),\n                Field.alias.replaceTextFromList(rootNode, this),\n                collectTraitsFrom(rootNode, tags),\n                Pf2eAction.prerequisites.transformTextFrom(rootNode, \", \", this),\n                Field.requirements.replaceTextFrom(rootNode, this),\n                Pf2eAction.frequency.getFrequencyFrom(rootNode, this),\n                Pf2eAction.activity.getActivityFrom(rootNode, this),\n                actionType == null ? null : actionType.build(this));\n    }\n\n    public enum Pf2eAction implements Pf2eJsonNodeReader {\n        activity,\n        actionType,\n        cost,\n        frequency,\n        info,\n        prerequisites,\n        trigger\n    }\n\n    @RegisterForReflection\n    static class ActionType {\n        public Boolean basic;\n        public Boolean item;\n        public Skill skill;\n        public List<String> ancestry;\n        public List<String> archetype;\n        public List<String> heritage;\n        public List<String> versatileHeritage;\n        @JsonProperty(\"class\")\n        public List<String> classType;\n        public List<String> subclass;\n        public List<String> variantrule;\n\n        public void addTags(JsonSource convert, Tags tags) {\n            if (isBasic()) {\n                tags.add(\"action\", \"basic\");\n            }\n            if (isItem()) {\n                tags.add(\"action\", \"item\");\n            }\n            if (ancestry != null) {\n                ancestry.forEach(c -> tags.add(\"action\", \"ancestry\", c));\n            }\n            if (archetype != null) {\n                archetype.forEach(c -> tags.add(\"action\", \"archetype\", c));\n            }\n            if (classType != null) {\n                classType.forEach(c -> tags.add(\"action\", \"class\", c));\n            }\n        }\n\n        public boolean isBasic() {\n            return basic != null && basic;\n        }\n\n        public boolean isItem() {\n            return item != null && item;\n        }\n\n        public QuteAction.ActionType build(JsonSource convert) {\n            return new QuteAction.ActionType(isBasic(), isItem(),\n                    skill == null ? null\n                            : skill.buildString(convert),\n                    classType == null ? null\n                            : classType.stream()\n                                    .map(s -> convert.linkify(Pf2eIndexType.classtype, s))\n                                    .collect(Collectors.toList()),\n                    subclass == null ? null\n                            : subclass.stream()\n                                    .map(this::createSubclassLink)\n                                    .map(s -> convert.linkify(Pf2eIndexType.classtype, s))\n                                    .collect(Collectors.toList()),\n                    archetype == null ? null\n                            : archetype.stream()\n                                    .map(s -> convert.linkify(Pf2eIndexType.archetype, s))\n                                    .collect(Collectors.toList()),\n                    ancestry == null ? null\n                            : ancestry.stream()\n                                    .map(this::createAncestryLink)\n                                    .map(s -> convert.linkify(Pf2eIndexType.ancestry, s))\n                                    .collect(Collectors.toList()),\n                    heritage == null ? null\n                            : heritage.stream()\n                                    .map(this::createHeritageLink)\n                                    .collect(Collectors.toList()),\n                    versatileHeritage == null ? null\n                            : versatileHeritage.stream()\n                                    .map(this::createVersatileHeritageLink)\n                                    .collect(Collectors.toList()),\n                    variantrule == null ? null\n                            : variantrule.stream()\n                                    .map(s -> convert.linkify(Pf2eIndexType.variantrule, s))\n                                    .collect(Collectors.toList()));\n        }\n\n        private String createSubclassLink(String subclassName) {\n            String[] cSrc = this.classType.get(0).split(\"\\\\|\");\n            String[] scSrc = subclassName.split(\"\\\\|\");\n            return String.format(\"%s|%s|%s|%s|%s\",\n                    cSrc[0],\n                    cSrc.length > 1 ? cSrc[1] : \"\",\n                    scSrc[0],\n                    scSrc[0],\n                    scSrc.length > 1 ? scSrc[1] : \"\");\n        }\n\n        private String createAncestryLink(String ancestry) {\n            String[] aSrc = ancestry.split(\"\\\\|\");\n            return String.format(\"%s|%s\",\n                    aSrc[0],\n                    aSrc.length > 1 ? aSrc[1] : \"\");\n        }\n\n        private String createHeritageLink(String heritage) {\n            String[] aSrc = this.ancestry.get(0).split(\"\\\\|\");\n            String[] hSrc = heritage.split(\"\\\\|\");\n            return String.format(\"%s|%s|%s|%s|%s|\",\n                    aSrc[0],\n                    aSrc.length > 1 ? aSrc[1] : \"\",\n                    hSrc[0],\n                    hSrc[0],\n                    hSrc.length > 1 ? hSrc[1] : \"\");\n        }\n\n        private String createVersatileHeritageLink(String versatile) {\n            String[] aSrc = (this.ancestry == null ? \"Human|CRB\" : this.ancestry.get(0))\n                    .split(\"\\\\|\");\n            String[] vSrc = versatile.split(\"\\\\|\");\n            return String.format(\"%s|%s|%s|%s|%s|\",\n                    aSrc[0],\n                    aSrc.length > 1 ? aSrc[1] : \"\",\n                    vSrc[0],\n                    vSrc[0],\n                    vSrc.length > 1 ? vSrc[1] : \"\");\n        }\n    }\n\n    @RegisterForReflection\n    static class Skill {\n        public List<String> trained;\n        public List<String> untrained;\n        public List<String> expert;\n        public List<String> legendary;\n\n        public String buildString(JsonSource convert) {\n            List<String> allSkills = new ArrayList<>();\n            if (untrained != null) {\n                List<String> inner = new ArrayList<>();\n                untrained.forEach(s -> inner.add(convert.linkify(Pf2eIndexType.skill, s)));\n                allSkills.add(String.format(\"%s (untrained)\", String.join(\", \", inner)));\n            }\n            if (trained != null) {\n                List<String> inner = new ArrayList<>();\n                trained.forEach(s -> inner.add(convert.linkify(Pf2eIndexType.skill, s)));\n                allSkills.add(String.format(\"%s (trained)\", String.join(\", \", inner)));\n            }\n            if (expert != null) {\n                List<String> inner = new ArrayList<>();\n                expert.forEach(s -> inner.add(convert.linkify(Pf2eIndexType.skill, s)));\n                allSkills.add(String.format(\"%s (expert)\", String.join(\", \", inner)));\n            }\n            if (legendary != null) {\n                List<String> inner = new ArrayList<>();\n                legendary.forEach(s -> inner.add(convert.linkify(Pf2eIndexType.skill, s)));\n                allSkills.add(String.format(\"%s (legendary)\", String.join(\", \", inner)));\n            }\n            return String.join(\"; \", allSkills);\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteAffliction.java",
    "content": "package dev.ebullient.convert.tools.pf2e;\n\nimport static dev.ebullient.convert.StringUtil.isPresent;\nimport static dev.ebullient.convert.StringUtil.toTitleCase;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.function.Predicate;\nimport java.util.stream.Collectors;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.StringUtil;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteAffliction;\n\npublic class Json2QuteAffliction extends Json2QuteBase {\n\n    public Json2QuteAffliction(Pf2eIndex index, Pf2eIndexType type, JsonNode rootNode) {\n        super(index, type, rootNode);\n    }\n\n    @Override\n    protected QuteAffliction buildQuteNote() {\n        return Pf2eAffliction.createAffliction(rootNode, this, getSources());\n    }\n\n    public enum Pf2eAffliction implements Pf2eJsonNodeReader {\n        name,\n        DC,\n        duration,\n        level,\n        maxDuration,\n        note,\n        onset,\n        savingThrow,\n        stage,\n        stages,\n        temptedCurse,\n        type,\n        entries,\n        entry;\n\n        /**\n         * Example JSON input, with an embedded affliction that does not have nested affliction data:\n         *\n         * <pre>\n         *     \"type\": \"affliction\",\n         *     \"name\": \"Goblin Pox\",\n         *     \"traits\": [\"disease\"],\n         *     \"level\": 1,\n         *     \"DC\": 22,\n         *     \"onset\": \"1d4 days\"\n         *     \"savingThrow\": \"Will\",\n         *     \"note\": \"Goblins and dogs are immune\",\n         *     \"stages\": [\n         *       {\"stage\": 1, \"entry\": \"sickened 1\", \"duration\": \"1 round\"},\n         *       {\"stage\": 2, \"entry\": \"sickened 1 and slowed 1\", \"duration\": \"1 round\"}\n         *     ],\n         *     \"entries\": [\n         *       \"Whenever you gain the frightened condition while in the Baffled Lowlands, increase the value by 1\"\n         *     ],\n         * </pre>\n         * <p>\n         * Example JSON input, with a standalone affliction that does have nested affliction data:\n         * </p>\n         *\n         * <pre>\n         *     \"name\": \"Blightburn Sickness\",\n         *     \"source\": \"TV\",\n         *     \"page\": 45,\n         *     \"type\": \"Disease\",\n         *     \"traits\": [\"disease\"],\n         *     \"level\": \"Level Varies\",\n         *     \"entries\": [\n         *       \"Caused by exposure to blightburn crystal, blightburn sickness burns and dissolves from within.\",\n         *       {\n         *         \"type\": \"affliction\",\n         *         \"onset\": \"1d4 days\",\n         *         \"DC\": 22,\n         *         \"savingThrow\": \"Will\",\n         *         \"note\": \"Goblins and dogs are immune\",\n         *         \"stages\": [\n         *           {\"stage\": 1, \"entry\": \"sickened 1\", \"duration\": \"1 round\"},\n         *           {\"stage\": 2, \"entry\": \"sickened 1 and slowed 1\", \"duration\": \"1 round\"}\n         *         ],\n         *         \"entries\": [\n         *           \"Whenever you gain the frightened condition while in the Baffled Lowlands, increase the value by 1\"\n         *         ],\n         *       }\n         *     ],\n         * </pre>\n         */\n        private static QuteAffliction createAffliction(\n                JsonNode node, JsonSource convert, Pf2eSources sources) {\n            boolean isEmbedded = sources == null;\n\n            // Sometimes the affliction data is nested as an entry within the parent node.\n            Optional<JsonNode> nestedAfflictionNode = Optional.ofNullable(getNestedAffliction(node));\n            if (!isEmbedded && nestedAfflictionNode.isEmpty()) {\n                // For standalone notes, we should always have a nested affliction node.\n                convert.tui().errorf(\"Unable to extract affliction entry from %s\", node.toPrettyString());\n                return null;\n            }\n            JsonNode dataNode = nestedAfflictionNode.orElse(node);\n\n            Tags tags = new Tags(sources);\n            Collection<String> traits = convert.collectTraitsFrom(node, tags);\n\n            Optional<String> afflictionLevel = level.intFrom(node).map(Objects::toString);\n            afflictionLevel.ifPresent(lv -> tags.add(\"affliction\", \"level\", lv));\n\n            String temptedCurseText = temptedCurse.transformTextFrom(node, \"\\n\", convert);\n            Optional<String> afflictionType = type.getTextFrom(node)\n                    .filter(s -> !s.equalsIgnoreCase(\"affliction\"))\n                    .filter(StringUtil::isPresent);\n            afflictionType.ifPresent(type -> {\n                if (isPresent(temptedCurseText)) {\n                    tags.add(\"affliction\", type, \"tempted\");\n                } else {\n                    tags.add(\"affliction\", type);\n                }\n            });\n\n            Optional<String> afflictionName = name.getTextFrom(node);\n\n            return new QuteAffliction(\n                    sources,\n                    // Standalone notes must have a valid affliction name so that we can name the file\n                    isEmbedded ? afflictionName.orElse(\"\") : afflictionName.orElseThrow(),\n                    // Any entries which were alongside the nested affliction block\n                    nestedAfflictionNode.isEmpty()\n                            ? List.of()\n                            : entries.streamFrom(node)\n                                    .filter(Predicate.not(AppendTypeValue.affliction::isBlockTypeOf))\n                                    .collect(\n                                            ArrayList<String>::new,\n                                            (acc, n) -> convert.appendToText(acc, n, \"##\"),\n                                            ArrayList::addAll),\n                    tags,\n                    traits,\n                    Field.alias.replaceTextFromList(dataNode, convert),\n                    // Level may be e.g. \"varies\"\n                    afflictionLevel.or(() -> level.getTextFrom(node))\n                            // Fix some data irregularities\n                            .map(s -> s.equalsIgnoreCase(\", level varies\") ? \"Level Varies\" : s)\n                            .orElse(null),\n                    afflictionType.orElse(null),\n                    maxDuration.replaceTextFrom(dataNode, convert),\n                    onset.replaceTextFrom(dataNode, convert),\n                    // DC can be either an int or a custom string. Only populate the object if any of the contained\n                    // fields are present\n                    savingThrow.getTextFrom(dataNode)\n                            .or(() -> DC.getTextFrom(dataNode))\n                            .or(() -> DC.intFrom(dataNode).map(Objects::toString))\n                            .map(StringUtil::isPresent)\n                            .map(unused -> new QuteAffliction.QuteAfflictionSave(\n                                    DC.intOrNull(dataNode),\n                                    savingThrow.getTextFrom(dataNode)\n                                            .map(s -> s.contains(\" \") ? s : toTitleCase(s)).orElse(null),\n                                    DC.getTextFrom(dataNode).map(convert::replaceText).orElse(null)))\n                            .orElse(null),\n                    entries.transformTextFrom(dataNode, \"\\n\", convert),\n                    temptedCurseText,\n                    note.getTextFrom(dataNode).map(convert::replaceText).map(List::of).orElse(List.of()),\n                    stages.streamFrom(dataNode)\n                            .map(n -> Map.entry(\"Stage %s\".formatted(stage.getTextOrDefault(n, \"1\")), n))\n                            .collect(Collectors.toMap(\n                                    Map.Entry::getKey,\n                                    e -> new QuteAffliction.QuteAfflictionStage(\n                                            duration.replaceTextFrom(e.getValue(), convert),\n                                            entry.transformTextFrom(e.getValue(), \"\\n\", convert, e.getKey())),\n                                    (x, y) -> y,\n                                    LinkedHashMap::new)),\n                    isEmbedded,\n                    convert);\n        }\n\n        static QuteAffliction createInlineAffliction(JsonNode node, JsonSource convert) {\n            return createAffliction(node, convert, null);\n        }\n\n        /** Try to extract the affliction node from the entries. Returns null if we couldn't extract one. */\n        private static JsonNode getNestedAffliction(JsonNode node) {\n            if (!entries.isArrayIn(node)) {\n                return null;\n            }\n            List<JsonNode> topLevelAfflictions = Pf2eAffliction.entries.streamFrom(node)\n                    .filter(AppendTypeValue.affliction::isBlockTypeOf).toList();\n            if (topLevelAfflictions.size() != 1) {\n                return null;\n            }\n            return topLevelAfflictions.get(0);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteArchetype.java",
    "content": "package dev.ebullient.convert.tools.pf2e;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteArchetype;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteFeat;\n\npublic class Json2QuteArchetype extends Json2QuteBase {\n\n    public Json2QuteArchetype(Pf2eIndex index, JsonNode rootNode) {\n        super(index, Pf2eIndexType.archetype, rootNode);\n    }\n\n    @Override\n    protected QuteArchetype buildQuteResource() {\n        Tags tags = new Tags(sources);\n        List<String> text = new ArrayList<>();\n\n        appendToText(text, SourceField.entries.getFrom(rootNode), \"##\");\n\n        List<String> benefits = ArchetypeField.benefits.getListOfStrings(rootNode, tui());\n        benefits.forEach(b -> tags.add(\"archetype\", \"benefit\", b));\n\n        int dedicationLevel = ArchetypeField.dedicationLevel.intOrDefault(rootNode, 2);\n\n        return new QuteArchetype(sources, text, tags,\n                collectTraitsFrom(rootNode, tags),\n                dedicationLevel,\n                benefits,\n                getFeatures(dedicationLevel));\n    }\n\n    List<String> getFeatures(int dedicationLevel) {\n        List<String> extraFeats = ArchetypeField.extraFeats.getListOfStrings(rootNode, tui());\n        Set<String> indexedFeats = index().featKeys(sources.getKey());\n\n        List<QuteFeat> quteFeats = new ArrayList<>();\n        List<String> featsRemaining = new ArrayList<>(extraFeats);\n\n        if (indexedFeats != null) {\n            indexedFeats.forEach(f -> {\n                JsonNode feat = index.getIncludedNode(f);\n                if (feat != null) {\n                    String needle = f.substring(\"feat\".length());\n                    Optional<String> extra = extraFeats.stream()\n                            // Sometimes there is a class note inserted: 10|Feat Name (Wizard)|Source\n                            // if so, attempt to recognize both with and without\n                            .flatMap(n -> (n.contains(\" (\")\n                                    ? List.of(n, n.replaceAll(\" \\\\(.*\\\\)\", \"\"))\n                                    : List.of(n)).stream())\n                            // Find the key that matches the ending: Feat Name|Source\n                            .filter(n -> n.endsWith(needle))\n                            .findFirst();\n\n                    // Find either the adjusted archetype field level or the Feat level\n                    String level = Json2QuteFeat.Pf2eFeat.level.getTextOrDefault(feat, \"1\");\n                    if (extra.isPresent()) {\n                        String extraKey = extra.get();\n                        level = extraKey.substring(0, extraKey.indexOf(\"|\"));\n                        featsRemaining.remove(extraKey);\n                    }\n                    quteFeats.add(createQuteFeat(feat, level));\n                }\n            });\n        }\n\n        featsRemaining.stream()\n                .map(this::findFeat)\n                .filter(Objects::nonNull)\n                .forEach(quteFeats::add);\n\n        quteFeats.sort((a, b) -> {\n            int compare = Integer.compare(Integer.parseInt(a.level), Integer.parseInt(b.level));\n            if (compare == 0) {\n                String aName = a.getName().toLowerCase();\n                String bName = b.getName().toLowerCase();\n                if (aName.equals(bName)) {\n                    return a.getSource().compareTo(b.getSource());\n                } else if (aName.contains(\"dedication\") && bName.contains(\"dedication\")) {\n                    return aName.compareTo(bName);\n                } else if (aName.contains(\"dedication\")) {\n                    return -1;\n                } else if (bName.contains(\"dedication\")) {\n                    return 1;\n                }\n                return aName.compareTo(bName);\n            }\n            return compare;\n        });\n\n        return quteFeats.stream()\n                .map(x -> render(x, x.note != null && x.note.contains(\"[!pf2-note] This version of\")))\n                .collect(Collectors.toList());\n    }\n\n    QuteFeat createQuteFeat(JsonNode feat, String level) {\n        Json2QuteFeat json2Qute = new Json2QuteFeat(index, feat);\n        return json2Qute.buildArchetype(sources.getName(), level);\n    }\n\n    QuteFeat findFeat(String levelKey) {\n        String[] parts = levelKey.split(\"\\\\|\");\n        String key = Pf2eIndexType.feat.createKey(parts[1], parts[2]);\n        JsonNode feat = index.getIncludedNode(key);\n\n        if (feat == null) {\n            tui().errorf(\"Could not find feat matching %s\", levelKey);\n            return null;\n        }\n        Json2QuteFeat json2Qute = new Json2QuteFeat(index, feat);\n        return json2Qute.buildArchetype(sources.getName(), parts[0]);\n    }\n\n    String render(QuteFeat quteFeat, boolean archetypeFeat) {\n        List<String> inner = new ArrayList<>();\n        renderEmbeddedTemplate(inner, quteFeat, \"feat\", List.of(\n                String.format(\"title: %s, Feat %s\", quteFeat.getName(), quteFeat.level + (archetypeFeat ? \"*\" : \"\")),\n                \"collapse: closed\"));\n\n        return String.join(\"\\n\", inner);\n    }\n\n    enum ArchetypeField implements Pf2eJsonNodeReader {\n        benefits,\n        dedicationLevel,\n        extraFeats,\n        miscTags,\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteBackground.java",
    "content": "package dev.ebullient.convert.tools.pf2e;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteBackground;\n\npublic class Json2QuteBackground extends Json2QuteBase {\n\n    public Json2QuteBackground(Pf2eIndex index, JsonNode rootNode) {\n        super(index, Pf2eIndexType.background, rootNode);\n    }\n\n    @Override\n    protected QuteBackground buildQuteResource() {\n        Tags tags = new Tags(sources);\n        List<String> text = new ArrayList<>();\n\n        appendToText(text, SourceField.entries.getFrom(rootNode), \"##\");\n\n        Pf2eBackground.boosts.getListOfStrings(rootNode, tui())\n                .stream()\n                .filter(b -> !b.equalsIgnoreCase(\"Free\"))\n                .forEach(s -> tags.add(\"background\", \"boost\", s));\n\n        Pf2eBackground.skills.getListOfStrings(rootNode, tui())\n                .forEach(s -> tags.add(\"background\", \"skill\", s));\n\n        Pf2eBackground.feat.getListOfStrings(rootNode, tui())\n                .forEach(s -> tags.add(\"background\", \"feat\", s));\n\n        return new QuteBackground(sources, text, tags);\n    }\n\n    enum Pf2eBackground implements Pf2eJsonNodeReader {\n        boosts,\n        skills,\n        feat,\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteBase.java",
    "content": "package dev.ebullient.convert.tools.pf2e;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase;\nimport dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteNote;\n\npublic abstract class Json2QuteBase implements JsonSource {\n    protected final Pf2eIndex index;\n    protected final Pf2eIndexType type;\n    protected final JsonNode rootNode;\n    protected final Pf2eSources sources;\n\n    public Json2QuteBase(Pf2eIndex index, Pf2eIndexType type, JsonNode rootNode) {\n        this(index, type, rootNode, Pf2eSources.findOrTemporary(type, rootNode));\n    }\n\n    public Json2QuteBase(Pf2eIndex index, Pf2eIndexType type, JsonNode rootNode, Pf2eSources sources) {\n        this.index = index;\n        this.type = type;\n        this.rootNode = rootNode;\n        this.sources = sources;\n    }\n\n    @Override\n    public Pf2eIndex index() {\n        return index;\n    }\n\n    @Override\n    public Pf2eSources getSources() {\n        return sources;\n    }\n\n    public Pf2eQuteBase build() {\n        boolean pushed = parseState().push(getSources(), rootNode);\n        try {\n            return buildQuteResource();\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    public Pf2eQuteNote buildNote() {\n        boolean pushed = parseState().push(getSources(), rootNode);\n        try {\n            return buildQuteNote();\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    protected Pf2eQuteBase buildQuteResource() {\n        tui().warnf(\"The default buildQuteResource method was called for %s. Was this intended?\", sources.toString());\n        return null;\n    }\n\n    protected Pf2eQuteNote buildQuteNote() {\n        tui().warnf(\"The default buildQuteNote method was called for %s. Was this intended?\", sources.toString());\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteBook.java",
    "content": "package dev.ebullient.convert.tools.pf2e;\n\nimport static dev.ebullient.convert.StringUtil.toAnchorTag;\nimport static dev.ebullient.convert.StringUtil.toTitleCase;\n\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteNote;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteBook;\nimport io.quarkus.runtime.annotations.RegisterForReflection;\n\npublic class Json2QuteBook extends Json2QuteBase {\n\n    final String bookRelativePath;\n    final JsonNode dataNode;\n\n    public Json2QuteBook(Pf2eIndex index, Pf2eIndexType type, JsonNode rootNode, JsonNode dataNode) {\n        super(index, type, rootNode);\n        this.dataNode = dataNode;\n        this.bookRelativePath = slugify(sources.getName());\n    }\n\n    /**\n     * From index entry and supporting data, construct a set of pages for the book.\n     * Page state has to be maintained.\n     */\n    public List<Pf2eQuteNote> buildBook() {\n        boolean pushed = parseState().push(getSources()); // set source\n        try {\n            QuteBook.BookInfo bookInfo = new QuteBook.BookInfo();\n            bookInfo.author = Pf2eBook.author.getTextOrEmpty(rootNode);\n            bookInfo.group = Pf2eBook.group.getTextOrEmpty(rootNode);\n            bookInfo.published = Pf2eBook.published.getTextOrEmpty(rootNode);\n\n            Tags coverTags = new Tags(sources);\n            coverTags.add(cfg().tagOf(\"book\",\n                    Field.group.getTextOrDefault(rootNode, \"ungrouped\"),\n                    bookRelativePath));\n\n            String coverUrl = Pf2eBook.coverUrl.getTextOrEmpty(rootNode);\n            if (coverUrl != null) {\n                Path coverPath = Path.of(coverUrl);\n                bookInfo.cover = Pf2eSources.buildImageRef(Pf2eIndexType.book, index, coverPath, sources.getName());\n            }\n\n            // Find coverNode and book sections\n            Map<String, JsonNode> bookSections = new HashMap<>();\n            for (JsonNode node : Pf2eBook.data.iterateArrayFrom(dataNode)) {\n                String name = SourceField.name.getTextOrEmpty(node);\n                if (name.isEmpty()) {\n                    continue;\n                }\n                bookSections.put(name, node);\n            }\n\n            List<Pf2eQuteNote> pages = new ArrayList<>();\n            List<String> coverText = new ArrayList<>();\n\n            for (JsonNode n : Pf2eBook.contents.iterateArrayFrom(rootNode)) {\n                String name = SourceField.name.getTextOrEmpty(n);\n                if (\"Cover\".equals(name)) {\n                    continue;\n                }\n                Ordinal ordinal = Pf2eBook.ordinal.fieldFromTo(n, Ordinal.class, tui());\n\n                String prefix = \"\";\n                if (ordinal != null) {\n                    prefix = String.format(\"%s %s: \", toTitleCase(ordinal.type), ordinal.identifier);\n                }\n\n                final String sectionTitle = String.format(\"%s%s\", prefix, name);\n                final String sectionFilename = slugify(sectionTitle);\n                maybeAddBlankLine(coverText);\n                coverText.add(String.format(\"**[%s](%s%s/%s.md)**\", sectionTitle,\n                        index.rulesVaultRoot(), bookRelativePath, sectionFilename));\n                coverText.add(\"\");\n\n                List<String> sectionHeaders = Pf2eBook.headers.getListOfStrings(n, tui());\n                for (String header : sectionHeaders) {\n                    coverText.add(String.format(\"- [%s](%s%s/%s.md#%s)\", header,\n                            index.rulesVaultRoot(), bookRelativePath, sectionFilename,\n                            toAnchorTag(header)));\n                }\n\n                JsonNode sectionNode = bookSections.getOrDefault(sectionTitle, bookSections.get(name));\n                if (sectionNode == null) {\n                    tui().errorf(\"Unable to find section for %s\", sectionTitle);\n                } else {\n                    pages.add(chapterPage(sectionFilename, sectionNode));\n                }\n            }\n\n            // folder note / cover\n            pages.add(new QuteBook(sources.getName(), coverText, coverTags, bookRelativePath, bookInfo, List.of()));\n\n            return pages;\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    Pf2eQuteNote chapterPage(String name, JsonNode pageNode) {\n        boolean pushed = parseState().push(pageNode);\n        try {\n            Tags tags = new Tags(sources);\n            List<String> text = new ArrayList<>();\n\n            appendToText(text, pageNode, \"#\");\n\n            return new QuteBook(name, text, tags, bookRelativePath, null, List.of());\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    @RegisterForReflection\n    static class Ordinal {\n        public String type;\n        public Object identifier;\n    }\n\n    enum Pf2eBook implements Pf2eJsonNodeReader {\n        author,\n        contents,\n        coverUrl,\n        data,\n        group,\n        headers,\n        ordinal,\n        published,\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteCompose.java",
    "content": "package dev.ebullient.convert.tools.pf2e;\n\nimport static dev.ebullient.convert.StringUtil.toAnchorTag;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\n\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.ToolsIndex.TtrpgValue;\nimport dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteNote;\n\npublic class Json2QuteCompose extends Json2QuteBase {\n    final List<JsonNode> nodes = new ArrayList<>();\n    Pf2eSources currentSources;\n    final String title;\n\n    public Json2QuteCompose(Pf2eIndexType type, Pf2eIndex index, String title) {\n        super(index, type, null,\n                Pf2eSources.constructSyntheticSource(title));\n        currentSources = super.getSources();\n        this.title = title;\n    }\n\n    public void add(JsonNode node) {\n        nodes.add(node);\n        if (ComposeFields.alias.existsIn(node)) {\n            String name = SourceField.name.getTextOrEmpty(node);\n            JsonNode entries = ((ObjectNode) node).arrayNode()\n                    .add(String.format(\"See [%s](#%s)\", name, toAnchorTag(name)));\n\n            for (String alias : ComposeFields.alias.getListOfStrings(node, tui())) {\n                ObjectNode copy = (ObjectNode) copyNode(node);\n                copy.put(\"name\", alias);\n                copy.remove(\"alias\");\n                copy.set(\"entries\", entries);\n                nodes.add(copy);\n            }\n        }\n    }\n\n    @Override\n    public Pf2eSources getSources() {\n        return currentSources;\n    }\n\n    @Override\n    public Pf2eQuteNote buildNote() {\n        // Override because we don't have global or even current sources here\n        // We have to push/pop source-related state as we work through\n        // contents (appendElement)\n        Tags tags = new Tags();\n        List<String> text = new ArrayList<>();\n\n        nodes.sort(Comparator.comparing(SourceField.name::getTextOrEmpty));\n        for (JsonNode entry : nodes) {\n            appendElement(entry, text, tags);\n        }\n\n        return new Pf2eQuteNote(type,\n                title,\n                null,\n                String.join(\"\\n\", text),\n                tags);\n    }\n\n    private void appendElement(JsonNode entry, List<String> text, Tags tags) {\n        String key = TtrpgValue.indexKey.getTextOrEmpty(entry);\n        currentSources = Pf2eSources.findSources(key);\n        String name = SourceField.name.getTextOrEmpty(entry);\n\n        if (index.keyIsIncluded(key, entry)) {\n            boolean pushed = parseState().push(entry);\n            try {\n                tags.addSourceTags(currentSources);\n                maybeAddBlankLine(text);\n                text.add(\"## \" + replaceText(name));\n                text.add(String.format(\"_Source: %s_\", currentSources.getSourceText()));\n                maybeAddBlankLine(text);\n                appendToText(text, SourceField.entries.getFrom(entry), \"###\");\n                appendToText(text, SourceField.entry.getFrom(entry), \"###\");\n\n                // Special content for some types (added to text)\n                addDomainSpells(name, text);\n\n                maybeAddBlankLine(text);\n            } finally {\n                parseState().pop(pushed);\n            }\n        }\n    }\n\n    void addDomainSpells(String name, List<String> text) {\n        Collection<String> spells = index().domainSpells(name);\n        if (type != Pf2eIndexType.domain || spells.isEmpty()) {\n            return;\n        }\n        maybeAddBlankLine(text);\n        text.add(\"**Spells** \" + spells.stream()\n                .map(s -> index().getIncludedNode(s))\n                .sorted(Comparator.comparingInt(n -> Json2QuteSpell.Pf2eSpell.level.intOrDefault(n, 1)))\n                .map(Pf2eSources::findSources)\n                .map(s -> linkify(Pf2eIndexType.spell, s.getName() + \"|\" + s.primarySource()))\n                .collect(Collectors.joining(\", \")));\n    }\n\n    enum ComposeFields implements Pf2eJsonNodeReader {\n        alias\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteCreature.java",
    "content": "package dev.ebullient.convert.tools.pf2e;\n\nimport static dev.ebullient.convert.StringUtil.join;\n\nimport java.util.Collection;\nimport java.util.Comparator;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteCreature;\n\npublic class Json2QuteCreature extends Json2QuteBase {\n\n    public Json2QuteCreature(Pf2eIndex index, JsonNode rootNode) {\n        super(index, Pf2eIndexType.creature, rootNode);\n    }\n\n    @Override\n    protected QuteCreature buildQuteResource() {\n        return Pf2eCreature.create(rootNode, this);\n    }\n\n    /**\n     * Example JSON input:\n     *\n     * <pre>\n     *     \"perception\": {\"std\": 6},\n     *     \"defenses\": { ... },\n     *     \"skills\": {\n     *         \"athletics\": 30,\n     *         \"stealth\": {\n     *             \"std\": 36,\n     *             \"in forests\": 42,\n     *             \"note\": \"additional note\"\n     *         },\n     *         \"notes\": [\n     *             \"some note\"\n     *         ]\n     *     },\n     *     \"abilityMods\": {\n     *         \"str\": 10,\n     *         \"dex\": 10,\n     *         \"con\": 10,\n     *         \"int\": 10,\n     *         \"wis\": 10,\n     *         \"cha\": 10\n     *     },\n     *     \"languages\": { ... },\n     *     \"senses\": [ { ... } ],\n     *     \"attacks\": [ { ... } ],\n     * </pre>\n     */\n    enum Pf2eCreature implements Pf2eJsonNodeReader {\n        abilities,\n        abilityMods,\n        alignment,\n        alias,\n        attacks,\n        defenses,\n        description,\n        entries,\n        hasImages,\n        inflicts, // not actually present in any of the entries\n        isNpc,\n        items,\n        languages,\n        level,\n        notes,\n        perception,\n        rarity,\n        rituals,\n        senses,\n        size,\n        skills,\n        speed,\n        spellcasting,\n        std,\n        traits;\n\n        private static QuteCreature create(JsonNode node, JsonSource convert) {\n            Tags tags = new Tags(convert.getSources());\n            Collection<String> traits = convert.collectTraitsFrom(node, tags);\n            traits.addAll(alignment.getAlignmentsFrom(node, convert));\n\n            return new QuteCreature(convert.getSources(),\n                    entries.transformTextFrom(node, \"\\n\", convert, \"##\"),\n                    tags,\n                    traits,\n                    alias.replaceTextFromList(node, convert),\n                    description.replaceTextFrom(node, convert),\n                    level.intOrNull(node),\n                    perception.getObjectFrom(node).map(std::intOrThrow).orElse(null),\n                    defenses.getDefensesFrom(node, convert),\n                    languages.getLanguagesFrom(node, convert),\n                    skills.getSkillsFrom(node, convert),\n                    senses.getSensesFrom(node, convert),\n                    // Use a linked hash map to preserve insertion order\n                    abilityMods.streamProps(node).collect(Collectors.toMap(\n                            Map.Entry::getKey, e -> e.getValue().asInt(), (u, v) -> u, LinkedHashMap::new)),\n                    items.replaceTextFromList(node, convert),\n                    speed.getSpeedFrom(node, convert),\n                    attacks.getAttacksFrom(node, convert),\n                    abilities.getCreatureAbilitiesFrom(node, convert),\n                    spellcasting.getSpellcastingFrom(node, convert),\n                    rituals.getRitualsFrom(node, convert));\n        }\n\n        private QuteCreature.CreatureSkills getSkillsFrom(JsonNode source, JsonSource convert) {\n            return getObjectFrom(source)\n                    .map(n -> new QuteCreature.CreatureSkills(\n                            streamPropsExcluding(source, notes)\n                                    .map(e -> Pf2eNamedBonus.getNamedBonus(e.getKey(), e.getValue(), convert))\n                                    .toList(),\n                            notes.replaceTextFromList(n, convert)))\n                    .filter(c -> !c.skills().isEmpty() || !c.notes().isEmpty())\n                    .orElse(null);\n        }\n\n        private QuteCreature.CreatureLanguages getLanguagesFrom(JsonNode node, JsonSource convert) {\n            return getObjectFrom(node)\n                    .map(n -> new QuteCreature.CreatureLanguages(\n                            Pf2eCreatureLanguages.languages.getListOfStrings(n, convert.tui()),\n                            Pf2eCreatureLanguages.abilities.replaceTextFromList(n, convert),\n                            Pf2eCreatureLanguages.notes.replaceTextFromList(n, convert)))\n                    .filter(c -> !c.languages().isEmpty() || !c.abilities().isEmpty() || !c.notes().isEmpty())\n                    .orElse(null);\n        }\n\n        enum Pf2eCreatureLanguages implements Pf2eJsonNodeReader {\n            /** Known languages e.g. {@code [\"Common\", \"Sylvan\"]} */\n            languages,\n            /** Language-related abilities e.g. {@code [\"{@ability telepathy} 100 feet\", \"(understands its creator)\"]} */\n            abilities,\n            /** Notes e.g. {@code [\"one elemental language\", \"one planar language\"]} */\n            notes;\n        }\n\n        /** Returns a {@link QuteCreature.CreatureAbilities} (with empty lists if this field is not present). */\n        private QuteCreature.CreatureAbilities getCreatureAbilitiesFrom(JsonNode source, JsonSource convert) {\n            return getObjectFrom(source)\n                    .map(n -> new QuteCreature.CreatureAbilities(\n                            Pf2eCreatureAbilities.top.getAbilityOrAfflictionsFrom(n, convert),\n                            Pf2eCreatureAbilities.mid.getAbilityOrAfflictionsFrom(n, convert),\n                            Pf2eCreatureAbilities.bot.getAbilityOrAfflictionsFrom(n, convert)))\n                    .orElseGet(() -> new QuteCreature.CreatureAbilities(List.of(), List.of(), List.of()));\n        }\n\n        enum Pf2eCreatureAbilities implements Pf2eJsonNodeReader {\n            top,\n            mid,\n            bot;\n        }\n\n        private List<QuteCreature.CreatureSense> getSensesFrom(JsonNode source, JsonSource convert) {\n            return streamFrom(source)\n                    .filter(convert::isObjectNode)\n                    .map(n -> new QuteCreature.CreatureSense(\n                            Pf2eCreatureSense.name.getTextFrom(n).map(convert::replaceText).orElseThrow(),\n                            Pf2eCreatureSense.type.getTextFrom(n).map(convert::replaceText).orElse(null),\n                            Pf2eCreatureSense.range.intOrNull(n)))\n                    .toList();\n        }\n\n        enum Pf2eCreatureSense implements Pf2eJsonNodeReader {\n            /** Name of the sense, e.g. {@code \"scent\"} (required) */\n            name,\n            /** Type of sense, e.g. {@code \"precise\"}, {@code \"imprecise\"}, {@code \"vague\"}, or {@code \"other\"} */\n            type,\n            /** Range of the sense (in feet, usually), optional integer. */\n            range;\n        }\n\n        private List<QuteCreature.CreatureSpellcasting> getSpellcastingFrom(JsonNode source, JsonSource convert) {\n            return streamFrom(source)\n                    .map(n -> Pf2eCreatureSpellcasting.getSpellcasting(n, convert))\n                    .filter(spellcasting -> !spellcasting.ranks().isEmpty() || !spellcasting.constantRanks().isEmpty())\n                    .toList();\n        }\n\n        private List<QuteCreature.CreatureRitualCasting> getRitualsFrom(JsonNode source, JsonSource convert) {\n            return streamFrom(source)\n                    .map(n -> Pf2eCreatureSpellcasting.getRitual(n, convert))\n                    .filter(rituals -> !rituals.ranks().isEmpty())\n                    .toList();\n        }\n\n        enum Pf2eCreatureSpellcasting implements Pf2eJsonNodeReader {\n            /** e.g. {@code \"Champion Devotion Spells\"} */\n            name,\n            /** e.g. {@code \"see soul spells below\"} */\n            note,\n            /** Required - one of {@code \"arcane\"}, {@code \"divine\"}, {@code \"occult\"}, or {@code \"primal\"} */\n            tradition,\n            /** Required - DC for spell effects */\n            DC,\n\n            /** Rituals only. Array of ritual references - see {@link Pf2eCreatureSpellReference} */\n            rituals,\n\n            /** Required - one of {@code \"Innate\"}, {@code \"Prepared\"}, {@code \"Spontaneous\"}, or {@code \"Focus\"} */\n            type,\n            /** Integer - number of focus points available */\n            fp,\n            /** Integer - The spell attack bonus */\n            attack,\n            /** Used within {@link #entry} only, as a key for a block. */\n            constant,\n            /**\n             * An object where the keys are the spell rank or {@link #constant}, and the values are another object with\n             * keys described below.\n             */\n            entry,\n\n            /**\n             * Used within {@link #entry} only. Integer. The level that these spells are heightened to - usually the\n             * same as the key for this block, except for cantrips.\n             */\n            level,\n            /**\n             * Used within {@link #entry} only. Integer. The number of slots available to cast the spells within this\n             * block.\n             */\n            slots,\n            /**\n             * Used within {@link #entry} only. A list of spell references.\n             *\n             * @see Pf2eCreatureSpellReference\n             */\n            spells;\n\n            private static QuteCreature.CreatureRitualCasting getRitual(JsonNode source, JsonSource convert) {\n                return new QuteCreature.CreatureRitualCasting(\n                        tradition.getEnumValueFrom(source, QuteCreature.SpellcastingTradition.class),\n                        DC.intOrNull(source),\n                        rituals.streamFrom(source)\n                                .collect(Collectors.toMap(\n                                        n -> level.intOrNull(n),\n                                        n -> Stream.of(Pf2eCreatureSpellReference.getSpellReference(n, convert)),\n                                        Stream::concat))\n                                .entrySet().stream()\n                                .map(e -> new QuteCreature.CreatureSpells(e.getKey(), e.getValue().toList()))\n                                .toList());\n            }\n\n            private static QuteCreature.CreatureSpellcasting getSpellcasting(JsonNode source, JsonSource convert) {\n                return new QuteCreature.CreatureSpellcasting(\n                        name.getTextOrNull(source),\n                        type.getEnumValueFrom(source, QuteCreature.SpellcastingPreparation.class),\n                        tradition.getEnumValueFrom(source, QuteCreature.SpellcastingTradition.class),\n                        fp.intOrNull(source),\n                        attack.intOrNull(source),\n                        DC.intOrNull(source),\n                        note.replaceTextFromList(source, convert),\n                        entry.getSpellsFrom(source, convert),\n                        constant.getSpellsFrom(entry.getFromOrEmptyObjectNode(source), convert));\n            }\n\n            private List<QuteCreature.CreatureSpells> getSpellsFrom(JsonNode source, JsonSource convert) {\n                return streamPropsExcluding(source, constant)\n                        .map(e -> new QuteCreature.CreatureSpells(\n                                Integer.valueOf(e.getKey()),\n                                level.intOrNull(e.getValue()),\n                                slots.intOrNull(e.getValue()),\n                                spells.streamFrom(e.getValue())\n                                        .map(n -> Pf2eCreatureSpellReference.getSpellReference(n, convert))\n                                        .toList()))\n                        .filter(creatureSpells -> !creatureSpells.spells().isEmpty())\n                        .sorted(Comparator.comparing(QuteCreature.CreatureSpells::knownRank).reversed())\n                        .toList();\n            }\n        }\n\n        enum Pf2eCreatureSpellReference implements Pf2eJsonNodeReader {\n            /** e.g. {@code \"comprehend language\"}. */\n            name,\n            /** The book source for this spell, e.g. {@code \"BotD\"} */\n            source,\n            /** Integer, or {@code \"at will\"}. Amount of available casts. For spells only, not rituals. */\n            amount,\n            /** e.g. {@code [\"self only\"]} */\n            notes;\n\n            private static QuteCreature.CreatureSpellReference getSpellReference(JsonNode node, JsonSource convert) {\n                String spellName = name.getTextOrThrow(node);\n                return new QuteCreature.CreatureSpellReference(\n                        spellName,\n                        convert.linkify(Pf2eIndexType.spell, join(\"|\", spellName, source.getTextOrNull(node))),\n                        amount.getTextFrom(node)\n                                .filter(s -> s.equalsIgnoreCase(\"at will\"))\n                                .map(unused -> 0)\n                                .or(() -> amount.intFrom(node))\n                                .orElse(1),\n                        notes.replaceTextFromList(node, convert));\n            }\n        }\n\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteDeity.java",
    "content": "package dev.ebullient.convert.tools.pf2e;\n\nimport static dev.ebullient.convert.StringUtil.join;\nimport static dev.ebullient.convert.StringUtil.joinConjunct;\nimport static dev.ebullient.convert.StringUtil.joiningConjunct;\nimport static dev.ebullient.convert.StringUtil.toOrdinal;\nimport static dev.ebullient.convert.StringUtil.toTitleCase;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.TreeMap;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.StringUtil;\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.qute.NamedText;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.Pf2eJsonNodeReader.Pf2eAttack;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteDeity;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteInlineAttack;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteInlineAttack.AttackRangeType;\nimport io.quarkus.runtime.annotations.RegisterForReflection;\n\npublic class Json2QuteDeity extends Json2QuteBase {\n\n    public Json2QuteDeity(Pf2eIndex index, JsonNode rootNode) {\n        super(index, Pf2eIndexType.background, rootNode);\n    }\n\n    @Override\n    protected QuteDeity buildQuteResource() {\n        List<String> text = new ArrayList<>();\n        Tags tags = new Tags(sources);\n\n        Pf2eDeity.domains.getListOfStrings(rootNode, tui()).forEach(d -> tags.add(\"domain\", d, \"deity\"));\n        Pf2eDeity.alternateDomains.getListOfStrings(rootNode, tui()).forEach(d -> tags.add(\"domain\", d, \"deity\"));\n\n        String category = Pf2eDeity.category.getTextOrDefault(rootNode, \"Deity\");\n        tags.add(\"deity\", category);\n\n        appendToText(text, SourceField.entries.getFrom(rootNode), \"##\");\n\n        JsonNode alignNode = Pf2eDeity.alignment.getFrom(rootNode);\n\n        return new QuteDeity(sources, text, tags,\n                Field.alias.replaceTextFromList(rootNode, this),\n                category,\n                join(\", \", Pf2eDeity.pantheon.linkifyListFrom(rootNode, Pf2eIndexType.deity, this)),\n                join(\", \", Pf2eDeity.alignment.getAlignmentsFrom(alignNode, this)),\n                join(\", \", Pf2eDeity.followerAlignment.getAlignmentsFrom(alignNode, this)),\n                Pf2eDeity.areasOfConcern.transformTextFrom(rootNode, \", \", this),\n                commandmentToString(Pf2eDeity.edict.replaceTextFromList(rootNode, this)),\n                commandmentToString(Pf2eDeity.anathema.replaceTextFromList(rootNode, this)),\n                buildCleric(),\n                buildAvatar(tags),\n                buildIntercession());\n    }\n\n    private QuteDeity.QuteDivineIntercession buildIntercession() {\n        JsonNode node = Pf2eDeity.intercession.getFrom(rootNode);\n        if (node == null) {\n            return null;\n        }\n        QuteDeity.QuteDivineIntercession intercession = new QuteDeity.QuteDivineIntercession();\n        intercession.source = Pf2eSources.createEmbeddedSource(node).getSourceText();\n        intercession.flavor = Pf2eDeity.flavor.transformTextFrom(node, \"\\n\", this);\n\n        intercession.majorBoon = Pf2eDeity.majorBoon.transformTextFrom(node, \"\\n\", this);\n        intercession.moderateBoon = Pf2eDeity.moderateBoon.transformTextFrom(node, \"\\n\", this);\n        intercession.minorBoon = Pf2eDeity.minorBoon.transformTextFrom(node, \"\\n\", this);\n\n        intercession.majorCurse = Pf2eDeity.majorCurse.transformTextFrom(node, \"\\n\", this);\n        intercession.moderateCurse = Pf2eDeity.moderateCurse.transformTextFrom(node, \"\\n\", this);\n        intercession.minorCurse = Pf2eDeity.minorCurse.transformTextFrom(node, \"\\n\", this);\n\n        return intercession;\n    }\n\n    QuteDeity.QuteDeityCleric buildCleric() {\n        QuteDeity.QuteDeityCleric cleric = new QuteDeity.QuteDeityCleric();\n        cleric.divineFont = join(\" or \", Pf2eDeity.font.linkifyListFrom(rootNode, Pf2eIndexType.spell, this));\n\n        EntryAndSomething entryAndSomething = Pf2eDeity.divineAbility.fieldFromTo(rootNode, EntryAndSomething.class, tui());\n        if (entryAndSomething != null) {\n            cleric.divineAbility = entryAndSomething.buildAbilityString(this);\n        }\n        entryAndSomething = Pf2eDeity.divineSkill.fieldFromTo(rootNode, EntryAndSomething.class, tui());\n        if (entryAndSomething != null) {\n            cleric.divineSkill = entryAndSomething.buildSkillString(this);\n        }\n\n        entryAndSomething = Pf2eDeity.favoredWeapon.fieldFromTo(rootNode, EntryAndSomething.class, tui());\n        if (entryAndSomething != null) {\n            cleric.favoredWeapon = entryAndSomething.buildFavoredWeapon(this);\n        }\n\n        cleric.domains = join(\", \", Pf2eDeity.domains.linkifyListFrom(rootNode, Pf2eIndexType.domain, this));\n        cleric.alternateDomains = join(\", \",\n                Pf2eDeity.alternateDomains.linkifyListFrom(rootNode, Pf2eIndexType.domain, this));\n\n        cleric.spells = new TreeMap<>();\n        Map<String, List<String>> clericSpells = Pf2eDeity.spells.fieldFromTo(rootNode, Tui.MAP_STRING_LIST_STRING, tui());\n        if (clericSpells != null) {\n            clericSpells.forEach((k, v) -> cleric.spells.put(toOrdinal(k), v.stream()\n                    .map(s -> linkify(Pf2eIndexType.spell, s))\n                    .collect(Collectors.joining(\", \"))));\n        }\n\n        return cleric;\n    }\n\n    QuteDeity.QuteDivineAvatar buildAvatar(Tags tags) {\n        JsonNode avatarNode = Pf2eDeity.avatar.getFrom(rootNode);\n        if (avatarNode == null) {\n            return null;\n        }\n\n        QuteDeity.QuteDivineAvatar avatar = new QuteDeity.QuteDivineAvatar();\n        avatar.preface = replaceText(Pf2eDeity.preface.getTextOrEmpty(avatarNode));\n        avatar.name = linkify(Pf2eIndexType.spell, \"avatar||Avatar\") + \" of \" + sources.getName();\n\n        avatar.speed = Pf2eDeity.speed.getSpeedFrom(avatarNode, this);\n        if (Pf2eDeity.airWalk.booleanOrDefault(avatarNode, false)) {\n            avatar.speed.addAbility(linkify(Pf2eIndexType.spell, \"air walk\"));\n        }\n\n        List<String> immunityLinks = Pf2eDeity.immune.getListOfStrings(avatarNode, tui())\n                .stream()\n                .map(s -> {\n                    // some immunities are actually traits\n                    return (index.traitToSource(s) != null)\n                            ? linkify(Pf2eIndexType.trait, s)\n                            : linkify(Pf2eIndexType.condition, s);\n                })\n                .toList();\n\n        String immunities = joinConjunct(\" and \", immunityLinks);\n        if (!immunities.isEmpty()) {\n            avatar.speed.addAbility(\"immune to \" + immunities);\n        }\n        if (Pf2eDeity.ignoreTerrain.booleanOrDefault(avatarNode, false)) {\n            avatar.speed.addAbility(replaceText(\n                    \"ignore {@quickref difficult terrain||3|terrain} and {@quickref greater difficult terrain||3|terrain}\"));\n        }\n\n        avatar.shield = Pf2eDeity.shield.intFrom(avatarNode)\n                .map(\"shield (%d Hardness, can't be damaged)\"::formatted).orElse(null);\n\n        avatar.attacks = Stream.concat(\n                Pf2eDeity.melee.streamFrom(avatarNode).map(n -> Map.entry(n, AttackRangeType.MELEE)),\n                Pf2eDeity.ranged.streamFrom(avatarNode).map(n -> Map.entry(n, AttackRangeType.RANGED)))\n                .map(e -> buildAvatarAttack(e.getKey(), tags, e.getValue()))\n                .toList();\n        avatar.ability = Pf2eDeity.ability.streamFrom(avatarNode)\n                .map(this::buildAvatarAbility)\n                .collect(Collectors.toList());\n\n        return avatar;\n    }\n\n    private NamedText buildAvatarAbility(JsonNode abilityNode) {\n        return new NamedText(\n                SourceField.name.getTextOrEmpty(abilityNode),\n                SourceField.entries.transformTextFrom(abilityNode, \"; \", this));\n    }\n\n    private QuteInlineAttack buildAvatarAttack(JsonNode actionNode, Tags tags, AttackRangeType rangeType) {\n        Collection<String> traits = collectTraitsFrom(actionNode, tags);\n        traits.addAll(Pf2eDeity.preciousMetal.getListOfStrings(actionNode, tui()));\n        Pf2eDeity.traitNote.getTextFrom(actionNode).ifPresent(traits::add);\n\n        return new QuteInlineAttack(\n                replaceText(Pf2eAttack.name.getTextOrDefault(actionNode, \"attack\")),\n                Pf2eActivity.single.toQuteActivity(this, null),\n                rangeType,\n                Json2QuteItem.Pf2eWeaponData.getDamageString(actionNode, this),\n                Stream.of(Json2QuteItem.Pf2eWeaponData.damageType, Json2QuteItem.Pf2eWeaponData.damageType2)\n                        .map(field -> field.getTextOrEmpty(actionNode))\n                        .filter(StringUtil::isPresent)\n                        .toList(),\n                traits,\n                Pf2eDeity.note.replaceTextFrom(actionNode, this),\n                this);\n    }\n\n    String commandmentToString(List<String> edictOrAnathema) {\n        return String.join(\n                edictOrAnathema.stream().anyMatch(x -> x.contains(\",\")) ? \"; \" : \", \",\n                edictOrAnathema);\n    }\n\n    @RegisterForReflection\n    static class EntryAndSomething {\n        public String entry;\n        public List<String> abilities;\n        public List<String> skills;\n        public List<String> weapons;\n\n        String buildAbilityString(JsonSource convert) {\n            if (entry != null) {\n                return convert.replaceText(entry);\n            }\n            return abilities.stream().map(StringUtil::toTitleCase).collect(joiningConjunct(\" or \"));\n        }\n\n        public String buildSkillString(JsonSource convert) {\n            if (entry != null) {\n                return convert.replaceText(entry);\n            }\n            return skills.stream()\n                    .map(s -> convert.linkify(Pf2eIndexType.spell, toTitleCase(s)))\n                    .collect(Collectors.joining(\", \"));\n        }\n\n        public String buildFavoredWeapon(JsonSource convert) {\n            if (entry != null) {\n                return convert.replaceText(entry);\n            }\n            return weapons.stream()\n                    .map(w -> convert.linkify(Pf2eIndexType.item, w))\n                    .collect(Collectors.joining(\", \"));\n        }\n    }\n\n    enum Pf2eDeity implements Pf2eJsonNodeReader {\n        ability, // avatar\n        airWalk, // avatar\n        alignment,\n        alternateDomains,\n        anathema,\n        areasOfConcern,\n        avatar,\n        category,\n        cleric,\n        damage,\n        damageType,\n        damage2,\n        damageType2,\n        divineAbility, // cleric\n        divineSkill, // cleric\n        domains,\n        edict,\n        favoredWeapon, // cleric\n        flavor, // intercession\n        followerAlignment, // alignment\n        font, // cleric\n        ignoreTerrain, // avatar\n        immune, // avatar\n        intercession,\n        majorBoon(\"Major Boon\"),\n        moderateBoon(\"Moderate Boon\"),\n        minorBoon(\"Minor Boon\"),\n        majorCurse(\"Major Curse\"),\n        moderateCurse(\"Moderate Curse\"),\n        minorCurse(\"Minor Curse\"),\n        melee, // avatar\n        note, // avatar\n        pantheon,\n        preciousMetal, //avatar\n        preface, // avatar\n        range, // avatar\n        ranged, // avatar\n        rangedIncrement, // avatar\n        reload, // avatar\n        shield, // avatar\n        speed, // avatar\n        spells, // cleric\n        traitNote; // avatar\n\n        final String nodeName;\n\n        Pf2eDeity() {\n            this.nodeName = this.name();\n        }\n\n        Pf2eDeity(String nodeName) {\n            this.nodeName = nodeName;\n        }\n\n        public String nodeName() {\n            return nodeName;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteFeat.java",
    "content": "package dev.ebullient.convert.tools.pf2e;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteFeat;\n\npublic class Json2QuteFeat extends Json2QuteBase {\n\n    public Json2QuteFeat(Pf2eIndex index, JsonNode rootNode) {\n        super(index, Pf2eIndexType.feat, rootNode);\n    }\n\n    @Override\n    protected QuteFeat buildQuteResource() {\n        Tags tags = new Tags(sources);\n        List<String> text = new ArrayList<>();\n\n        appendToText(text, SourceField.entries.getFrom(rootNode), \"##\");\n\n        List<String> leadsTo = Pf2eFeat.leadsTo.getListOfStrings(rootNode, tui())\n                .stream()\n                .map(x -> linkify(Pf2eIndexType.feat, x))\n                .collect(Collectors.toList());\n\n        return new QuteFeat(sources, text, tags,\n                collectTraitsFrom(rootNode, tags),\n                Field.alias.replaceTextFromList(rootNode, this),\n                Pf2eFeat.level.getTextOrDefault(rootNode, \"1\"),\n                Pf2eFeat.access.transformTextFrom(rootNode, \", \", this),\n                Pf2eFeat.frequency.getFrequencyFrom(rootNode, this),\n                Pf2eFeat.activity.getActivityFrom(rootNode, this),\n                Pf2eFeat.trigger.transformTextFrom(rootNode, \", \", this),\n                Pf2eFeat.cost.transformTextFrom(rootNode, \", \", this),\n                Field.requirements.transformTextFrom(rootNode, \", \", this),\n                Pf2eFeat.prerequisites.transformTextFrom(rootNode, \", \", this),\n                Pf2eFeat.special.transformTextFrom(rootNode, \"\\n\", this),\n                null, leadsTo, true);\n    }\n\n    public QuteFeat buildArchetype(String archetypeName, String dedicationLevel) {\n        String featLevel = Pf2eFeat.level.getTextOrDefault(rootNode, \"1\");\n        List<String> text = new ArrayList<>();\n        Tags tags = new Tags();\n\n        String note = null;\n        if (dedicationLevel != featLevel) {\n            note = String.format(\n                    \"> [!pf2-note] This version of %s is intended for use with the %s Archetype. Its level has been changed accordingly.\",\n                    index.linkify(this.type, String.join(\"|\", List.of(sources.getName(), sources.primarySource()))),\n                    archetypeName);\n        }\n\n        appendToText(text, SourceField.entries.getFrom(rootNode), \"##\");\n\n        return new QuteFeat(sources, text, tags,\n                collectTraitsFrom(rootNode, tags),\n                List.of(),\n                dedicationLevel,\n                Pf2eFeat.access.transformTextFrom(rootNode, \", \", this),\n                Pf2eFeat.frequency.getFrequencyFrom(rootNode, this),\n                Pf2eFeat.activity.getActivityFrom(rootNode, this),\n                Pf2eFeat.trigger.transformTextFrom(rootNode, \", \", this),\n                Pf2eFeat.cost.transformTextFrom(rootNode, \", \", this),\n                Field.requirements.transformTextFrom(rootNode, \", \", this),\n                Pf2eFeat.prerequisites.transformTextFrom(rootNode, \", \", this),\n                Pf2eFeat.special.transformTextFrom(rootNode, \", \", this),\n                note, List.of(), true);\n    }\n\n    public enum Pf2eFeat implements Pf2eJsonNodeReader {\n        access,\n        activity,\n        archetype, // child of featType\n        cost,\n        featType,\n        frequency,\n        leadsTo,\n        level,\n        prerequisites,\n        special,\n        trigger\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteHazard.java",
    "content": "package dev.ebullient.convert.tools.pf2e;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.tools.JsonTextConverter;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.Json2QuteAbility.Pf2eAbility;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteDataGenericStat;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteHazard;\n\npublic class Json2QuteHazard extends Json2QuteBase {\n\n    public Json2QuteHazard(Pf2eIndex index, JsonNode rootNode) {\n        super(index, Pf2eIndexType.hazard, rootNode);\n    }\n\n    @Override\n    protected QuteHazard buildQuteResource() {\n        Tags tags = new Tags(sources);\n        List<String> text = new ArrayList<>();\n\n        appendToText(text, Pf2eHazard.description.getFrom(rootNode), \"##\");\n\n        return new QuteHazard(sources, text, tags,\n                collectTraitsFrom(rootNode, tags),\n                Pf2eHazard.level.getTextOrDefault(rootNode, \"0\"),\n                Pf2eHazard.disable.transformTextFrom(rootNode, \"\\n\", index),\n                Pf2eHazard.reset.transformTextFrom(rootNode, \"\\n\", index),\n                Pf2eHazard.routine.transformTextFrom(rootNode, \"\\n\", index),\n                Pf2eHazard.defenses.getDefensesFrom(rootNode, this),\n                Pf2eHazard.attacks.getAttacksFrom(rootNode, this),\n                Pf2eHazard.abilities.streamFrom(rootNode)\n                        .map(n -> Pf2eAbility.createEmbeddedAbility(n, this))\n                        .toList(),\n                Pf2eHazard.actions.getAbilityOrAfflictionsFrom(rootNode, this),\n                Pf2eHazard.stealth.getObjectFrom(rootNode)\n                        .map(n -> Pf2eHazardAttribute.buildStealth(n, this)).orElse(null),\n                Pf2eHazard.perception.getObjectFrom(rootNode)\n                        .map(n -> Pf2eHazardAttribute.buildPerception(n, this)).orElse(null));\n    }\n\n    enum Pf2eHazard implements Pf2eJsonNodeReader {\n        abilities,\n        actions,\n        attacks,\n        defenses,\n        description,\n        disable,\n        level,\n        perception,\n        reset,\n        routine,\n        stealth,\n    }\n\n    enum Pf2eHazardAttribute implements Pf2eJsonNodeReader {\n        dc,\n        bonus,\n        minProf,\n        notes;\n\n        static QuteHazard.QuteHazardStealth buildStealth(JsonNode node, JsonTextConverter<?> convert) {\n            return new QuteHazard.QuteHazardStealth(\n                    bonus.intOrNull(node),\n                    dc.intOrNull(node),\n                    minProf.getTextOrNull(node),\n                    notes.getTextFrom(node).map(convert::replaceText).orElse(null));\n        }\n\n        static QuteDataGenericStat.SimpleStat buildPerception(JsonNode node, JsonTextConverter<?> convert) {\n            return new QuteDataGenericStat.SimpleStat(\n                    bonus.intOrThrow(node),\n                    notes.getTextFrom(node).map(convert::replaceText).orElse(null));\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteItem.java",
    "content": "package dev.ebullient.convert.tools.pf2e;\n\nimport static dev.ebullient.convert.StringUtil.isPresent;\nimport static dev.ebullient.convert.StringUtil.join;\nimport static dev.ebullient.convert.StringUtil.toTitleCase;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map.Entry;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.qute.NamedText;\nimport dev.ebullient.convert.tools.JsonNodeReader;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteDataArmorClass;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteDataGenericStat.SimpleStat;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteDataHpHardnessBt;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteItem;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteItem.QuteItemActivate;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteItem.QuteItemArmorData;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteItem.QuteItemShieldData;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteItem.QuteItemWeaponData;\n\npublic class Json2QuteItem extends Json2QuteBase {\n    static final String ITEM_TAG = \"item\";\n\n    public Json2QuteItem(Pf2eIndex index, JsonNode rootNode) {\n        super(index, Pf2eIndexType.item, rootNode);\n    }\n\n    @Override\n    protected Pf2eQuteBase buildQuteResource() {\n        Tags tags = new Tags(sources);\n        List<String> text = new ArrayList<>();\n        List<String> aliases = new ArrayList<>(Field.alias.replaceTextFromList(rootNode, this));\n        Set<String> traits = collectTraitsFrom(rootNode, tags);\n\n        appendToText(text, SourceField.entries.getFrom(rootNode), \"##\");\n\n        String duration = Pf2eItem.duration.existsIn(rootNode)\n                ? SourceField.entry.getTextOrEmpty(Pf2eItem.duration.getFrom(rootNode))\n                : null;\n\n        return new QuteItem(sources, text, tags, traits, aliases,\n                buildActivate(),\n                getPrice(rootNode),\n                join(\", \", Pf2eItem.ammunition.linkifyListFrom(rootNode, Pf2eIndexType.item, this)),\n                Pf2eItem.level.getTextOrDefault(rootNode, \"0\"),\n                Pf2eItem.onset.transformTextFrom(rootNode, \", \", this),\n                replaceText(Pf2eItem.access.getTextOrEmpty(rootNode)),\n                duration,\n                getCategory(tags),\n                linkify(Pf2eIndexType.group, getGroup()),\n                Pf2eItem.hands.getTextOrEmpty(rootNode),\n                keysToList(List.of(Pf2eItem.usage, Pf2eItem.bulk)),\n                getContract(tags),\n                getShieldData(),\n                getArmorData(),\n                getWeaponData(tags),\n                getVariants(tags),\n                Pf2eItem.craftReq.transformTextFrom(rootNode, \"; \", this));\n    }\n\n    private String getPrice(JsonNode rootNode) {\n        JsonNode price = Pf2eItem.price.getFrom(rootNode);\n        if (price != null) {\n            StringBuilder sb = new StringBuilder();\n            String amount = Pf2eItem.amount.replaceTextFrom(price, this);\n            if (amount != null) {\n                sb.append(amount);\n            }\n            String coin = Pf2eItem.coin.replaceTextFrom(price, this);\n            if (coin != null) {\n                sb.append(\" \").append(coin);\n            }\n            return sb.toString().trim();\n        }\n        return \"\";\n    }\n\n    /**\n     * Example input JSON:\n     *\n     * <pre>\n     *     \"sheldData\": {\n     *         \"ac\": 2,\n     *         \"ac2\": 3,\n     *         \"hardness\": 5,\n     *         \"hp\": 20,\n     *         \"bt\": 10,\n     *         \"speedPen\": 10\n     *     }\n     * </pre>\n     *\n     * <p>\n     * `speedPen` is optional.\n     * </p>\n     */\n    private QuteItemShieldData getShieldData() {\n        JsonNode shieldNode = Pf2eItem.shieldData.getFrom(rootNode);\n        return shieldNode == null ? null\n                : new QuteItemShieldData(\n                        new QuteDataArmorClass(\n                                Pf2eItem.ac.intOrThrow(shieldNode),\n                                Pf2eItem.ac2.intOrNull(shieldNode)),\n                        new QuteDataHpHardnessBt(\n                                new QuteDataHpHardnessBt.HpStat(Pf2eItem.hp.intOrThrow(shieldNode)),\n                                new SimpleStat(Pf2eItem.hardness.intOrThrow(shieldNode)),\n                                Pf2eItem.bt.intOrThrow(shieldNode)),\n                        penalty(Pf2eItem.speedPen.getTextOrEmpty(shieldNode), \" ft.\"));\n    }\n\n    private QuteItemArmorData getArmorData() {\n        JsonNode armorDataNode = Pf2eItem.armorData.getFrom(rootNode);\n        if (armorDataNode == null) {\n            return null;\n        }\n\n        QuteItemArmorData armorData = new QuteItemArmorData();\n        Pf2eItem.ac.intFrom(armorDataNode).ifPresent(ac -> armorData.ac = new QuteDataArmorClass(ac));\n        armorData.dexCap = Pf2eItem.dexCap.bonusOrNull(armorDataNode);\n\n        armorData.strength = Pf2eItem.str.getTextOrDefault(armorDataNode, \"—\");\n\n        String checkPen = Pf2eItem.checkPen.getTextOrDefault(armorDataNode, null);\n        armorData.checkPenalty = penalty(checkPen, \"\");\n\n        String speedPen = Pf2eItem.speedPen.getTextOrDefault(armorDataNode, null);\n        armorData.speedPenalty = penalty(speedPen, \" ft.\");\n\n        return armorData;\n    }\n\n    private List<QuteItemWeaponData> getWeaponData(Tags tags) {\n        JsonNode weaponDataNode = Pf2eItem.weaponData.getFrom(rootNode);\n        if (weaponDataNode == null) {\n            return null;\n        }\n        List<QuteItemWeaponData> weaponDataList = new ArrayList<>();\n        weaponDataList.add(Pf2eWeaponData.buildWeaponData(weaponDataNode, this, tags));\n\n        JsonNode comboWeaponData = Pf2eItem.comboWeaponData.getFrom(rootNode);\n        if (comboWeaponData != null) {\n            weaponDataList.add(Pf2eWeaponData.buildWeaponData(comboWeaponData, this, tags));\n        }\n\n        return weaponDataList;\n    }\n\n    private List<QuteItem.QuteItemVariant> getVariants(Tags tags) {\n\n        JsonNode variantsNode = Pf2eItem.variants.getFrom(rootNode);\n        if (variantsNode == null)\n            return null;\n\n        List<QuteItem.QuteItemVariant> variantList = new ArrayList<>();\n\n        variantsNode.forEach(e -> {\n            QuteItem.QuteItemVariant variant = new QuteItem.QuteItemVariant();\n            variant.level = Pf2eItemVariant.level.intOrDefault(e, 0);\n            variant.variantType = Pf2eItemVariant.variantType.replaceTextFrom(e, this);\n            variant.price = getPrice(e);\n            variant.entries = new ArrayList<>();\n            appendToText(variant.entries, SourceField.entries.getFrom(e), null);\n            variant.craftReq = new ArrayList<>();\n            appendToText(variant.craftReq, Pf2eItemVariant.craftReq.getFrom(e), null);\n\n            variantList.add(variant);\n        });\n\n        return variantList;\n    }\n\n    private Collection<NamedText> getContract(Tags tags) {\n        JsonNode contractNode = Pf2eItem.contract.getFrom(rootNode);\n        if (contractNode == null) {\n            return null;\n        }\n\n        NamedText.SortedBuilder namedText = new NamedText.SortedBuilder();\n        for (Entry<String, JsonNode> e : iterableFields(contractNode)) {\n            if (e.getKey().equals(\"decipher\")) {\n                List<String> writing = toListOfStrings(e.getValue()).stream()\n                        .map(s -> linkify(Pf2eIndexType.skill, s))\n                        .collect(Collectors.toList());\n                namedText.add(\"Decipher Writing\", join(\", \", writing));\n            } else {\n                namedText.add(toTitleCase(e.getKey()), replaceText(e.getValue()));\n            }\n        }\n        ;\n        return namedText.build();\n    }\n\n    Collection<NamedText> keysToList(List<Pf2eItem> keys) {\n        NamedText.SortedBuilder namedText = new NamedText.SortedBuilder();\n        for (Pf2eItem k : keys) {\n            String value = k.getTextOrEmpty(rootNode);\n            if (!value.isEmpty()) {\n                namedText.add(k.properName(), replaceText(value));\n            }\n        }\n        return namedText.isEmpty() ? List.of() : namedText.build();\n    }\n\n    QuteItemActivate buildActivate() {\n        JsonNode activateNode = Pf2eItem.activate.getFrom(rootNode);\n        if (activateNode == null) {\n            return null;\n        }\n\n        QuteItemActivate activate = new QuteItemActivate();\n        activate.activity = Pf2eItem.activity.getActivityFrom(rootNode, this);\n        activate.components = Pf2eItem.components.transformTextFrom(activateNode, \", \", this);\n        activate.requirements = Field.requirements.replaceTextFrom(activateNode, this);\n        activate.frequency = Pf2eItem.frequency.getFrequencyFrom(activateNode, this);\n        activate.trigger = Pf2eItem.trigger.transformTextFrom(activateNode, \", \", this);\n        activate.requirements = Pf2eItem.requirements.transformTextFrom(activateNode, \", \", this);\n        return activate;\n    }\n\n    private String getGroup() {\n        // If weaponData and !comboWeaponData, use weaponData group.\n        // If not, use armorData group.\n        // If not, check if it's a Shield, then make it a Shield. If not, use the item.group.\n        if (Pf2eItem.weaponData.existsIn(rootNode) && !Pf2eItem.comboWeaponData.existsIn(rootNode)) {\n            return Pf2eWeaponData.group.getTextOrEmpty(Pf2eItem.weaponData.getFrom(rootNode));\n        }\n        if (Pf2eItem.armorData.existsIn(rootNode)) {\n            return Pf2eWeaponData.group.getTextOrEmpty(Pf2eItem.armorData.getFrom(rootNode));\n        }\n        String category = Pf2eItem.category.getTextOrEmpty(rootNode);\n        if (\"Shield\".equals(category)) {\n            return \"Shield\";\n        }\n        return Pf2eWeaponData.group.getTextOrEmpty(rootNode);\n    }\n\n    String getCategory(Tags tags) {\n        String category = Pf2eItem.category.getTextOrEmpty(rootNode);\n        String subcategory = Pf2eItem.subCategory.getTextOrEmpty(rootNode);\n        if (category == null) {\n            return null;\n        }\n        if (subcategory == null) {\n            tags.add(ITEM_TAG, \"category\", category);\n            return category;\n        }\n        tags.add(ITEM_TAG, \"category\", category, subcategory);\n        return subcategory;\n    }\n\n    private String penalty(String input, String suffix) {\n        if (!isPresent(input) || \"0\".equals(input)) {\n            return \"—\";\n        }\n        return (input.startsWith(\"-\") ? input : (\"-\" + input)) + suffix;\n    }\n\n    enum Pf2eItem implements Pf2eJsonNodeReader {\n        ac, // shieldData\n        ac2, // shieldData\n        access,\n        activate,\n        activity,\n        ammunition,\n        amount, // price\n        armorData,\n        bt, // shieldData\n        bulk,\n        category,\n        checkPen, // armorData\n        coin, // price\n        comboWeaponData,\n        components,\n        contract,\n        craftReq,\n        dexCap, // shieldData\n        duration,\n        frequency,\n        hands,\n        hp, // shieldData\n        hardness, // shieldData\n        level,\n        onset,\n        price,\n        requirements,\n        shieldData,\n        speedPen, // shieldData, armorData\n        str, // armorData\n        subCategory,\n        trigger,\n        usage,\n        variants,\n        weaponData;\n\n        String properName() {\n            return toTitleCase(this.nodeName());\n        }\n    }\n\n    enum Pf2eItemVariant implements Pf2eJsonNodeReader {\n        level,\n        price,\n        entries,\n        variantType,\n        craftReq\n    }\n\n    public enum Pf2eWeaponData implements Pf2eJsonNodeReader {\n        ammunition,\n        damage,\n        damageType,\n        damage2,\n        damageType2,\n        group,\n        range,\n        reload;\n\n        public static QuteItemWeaponData buildWeaponData(JsonNode source,\n                JsonSource convert, Tags tags) {\n\n            QuteItemWeaponData weaponData = new QuteItemWeaponData();\n            weaponData.traits = convert.collectTraitsFrom(source, tags);\n            weaponData.type = SourceField.type.getTextOrEmpty(source);\n            weaponData.damage = getDamageString(source, convert);\n\n            weaponData.ranged = new ArrayList<>();\n            String ammunition = Pf2eWeaponData.ammunition.getTextOrNull(source);\n            if (ammunition != null) {\n                weaponData.ranged.add(new NamedText(\"Ammunution\", convert.linkify(Pf2eIndexType.item, ammunition)));\n            }\n            String range = Pf2eWeaponData.range.getTextOrNull(source);\n            if (range != null) {\n                weaponData.ranged.add(new NamedText(\"Range\", range + \" ft.\"));\n            }\n            String reload = Pf2eWeaponData.reload.getTextOrNull(source);\n            if (reload != null) {\n                weaponData.ranged.add(new NamedText(\"Reload\", convert.replaceText(reload)));\n            }\n\n            String group = Pf2eWeaponData.group.getTextOrNull(source);\n            if (group != null) {\n                weaponData.group = convert.linkify(Pf2eIndexType.group, group);\n            }\n\n            return weaponData;\n        }\n\n        public static String getDamageString(JsonNode source, JsonSource convert) {\n            String damage = Pf2eWeaponData.damage.getTextOrNull(source);\n            String damage2 = Pf2eWeaponData.damage2.getTextOrNull(source);\n\n            String result = \"\";\n            if (damage != null) {\n                result += convert.replaceText(\"{@damage %s} %s\".formatted(\n                        damage,\n                        Pf2eWeaponData.damageType.getTextOrEmpty(source)));\n            }\n            if (damage2 != null) {\n                result += convert.replaceText(\"%s{@damage %s} %s\".formatted(\n                        damage == null ? \"\" : \" and \",\n                        damage2,\n                        Pf2eWeaponData.damageType2.getTextOrEmpty(source)));\n            }\n            return result;\n        }\n\n        static String getDamageType(JsonNodeReader damageType, JsonNode source) {\n            String value = damageType.getTextOrEmpty(source);\n            return switch (value) {\n                case \"A\" -> \"acid\";\n                case \"B\" -> \"bludgeoning\";\n                case \"C\" -> \"cold\";\n                case \"D\" -> \"bleed\";\n                case \"E\" -> \"electricity\";\n                case \"F\" -> \"fire\";\n                case \"H\" -> \"chaotic\";\n                case \"I\" -> \"poison\";\n                case \"L\" -> \"lawful\";\n                case \"M\" -> \"mental\";\n                case \"Mod\" -> \"modular\";\n                case \"N\" -> \"sonic\";\n                case \"O\" -> \"force\";\n                case \"P\" -> \"piercing\";\n                case \"R\" -> \"precision\";\n                case \"S\" -> \"slashing\";\n                case \"+\" -> \"positive\";\n                case \"-\" -> \"negative\";\n                default -> value;\n            };\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteRitual.java",
    "content": "package dev.ebullient.convert.tools.pf2e;\n\nimport static dev.ebullient.convert.StringUtil.joinConjunct;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteDataRange;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteRitual;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteRitual.QuteRitualCasting;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteRitual.QuteRitualChecks;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteSpell.QuteSpellTarget;\nimport io.quarkus.runtime.annotations.RegisterForReflection;\n\npublic class Json2QuteRitual extends Json2QuteSpell {\n    static final String RITUAL_TAG = \"ritual\";\n\n    public Json2QuteRitual(Pf2eIndex index, JsonNode rootNode) {\n        super(index, Pf2eIndexType.ritual, rootNode);\n    }\n\n    @Override\n    protected Pf2eQuteBase buildQuteResource() {\n        Tags tags = new Tags(sources);\n        List<String> text = new ArrayList<>();\n\n        appendToText(text, SourceField.entries.getFrom(rootNode), \"##\");\n\n        String level = Pf2eSpell.level.getTextOrDefault(rootNode, \"1\");\n        tags.add(RITUAL_TAG, level);\n\n        return new QuteRitual(sources, text, tags,\n                level, \"Ritual\",\n                collectTraitsFrom(rootNode, tags),\n                Field.alias.replaceTextFromList(rootNode, this),\n                getQuteRitualCast(),\n                getQuteRitualChecks(),\n                getQuteRitualSpellTarget(tags),\n                Field.requirements.transformTextFrom(rootNode, \", \", this),\n                null,\n                getHeightenedCast());\n    }\n\n    QuteSpellTarget getQuteRitualSpellTarget(Tags tags) {\n        String targets = replaceText(Pf2eSpell.targets.getTextOrEmpty(rootNode));\n        QuteDataRange range = Pf2eSpell.range.getRangeFrom(rootNode, this);\n        SpellArea area = Pf2eSpell.area.fieldFromTo(rootNode, SpellArea.class, tui());\n        if (targets == null && range == null && area == null) {\n            return null;\n        }\n\n        QuteSpellTarget spellTarget = new QuteSpellTarget();\n        if (targets != null) {\n            spellTarget.targets = replaceText(targets);\n        }\n        spellTarget.range = range;\n        if (area != null) {\n            spellTarget.area = area.entry;\n            area.types.forEach(t -> tags.add(RITUAL_TAG, \"area\", t));\n        }\n        return spellTarget;\n    }\n\n    QuteRitualCasting getQuteRitualCast() {\n        RitualSecondaryCaster casters = Pf2eSpell.secondaryCasters.fieldFromTo(rootNode, RitualSecondaryCaster.class, tui());\n\n        QuteRitualCasting quteCast = new QuteRitualCasting();\n        quteCast.duration = Pf2eSpell.cast.getDurationFrom(rootNode, this);\n        quteCast.cost = Pf2eSpell.cost.transformTextFrom(rootNode, \", \", this);\n        if (casters != null) {\n            quteCast.secondaryCasters = casters.buildString(this);\n        }\n        return quteCast;\n    }\n\n    QuteRitualChecks getQuteRitualChecks() {\n        RitualCheck primary = Pf2eSpell.primaryCheck.fieldFromTo(rootNode, RitualCheck.class, tui());\n        RitualCheck secondary = Pf2eSpell.secondaryCheck.fieldFromTo(rootNode, RitualCheck.class, tui());\n        if (primary == null && secondary == null) {\n            return null;\n        }\n\n        QuteRitualChecks checks = new QuteRitualChecks();\n\n        checks.primaryChecks = primary.buildPrimaryString(this);\n        if (secondary != null) {\n            checks.secondaryChecks = secondary.buildSecondaryString(this);\n        }\n\n        return checks;\n    }\n\n    @RegisterForReflection\n    public static class RitualSecondaryCaster {\n        public String entry;\n        public Integer number;\n        public String note;\n\n        public String buildString(JsonSource convert) {\n            // ${ritual.secondaryCasters.entry ? ritual.secondaryCasters.entry       : ritual.secondaryCasters.number}\n            // ${ritual.secondaryCasters.note  ? `, ${ritual.secondaryCasters.note}` : \"\"}\n            return String.format(\"%s%s\",\n                    entry == null ? number : convert.replaceText(entry),\n                    note == null ? \"\" : \" \" + note);\n        }\n    }\n\n    @RegisterForReflection\n    public static class RitualCheck {\n        public String prof;\n        public String entry;\n        public List<String> skills;\n        public List<String> mustBe;\n\n        public String buildPrimaryString(JsonSource convert) {\n            if (entry != null) {\n                return convert.replaceText(entry);\n            }\n            return String.format(\"%s (%s%s)\", skillsToString(convert), prof,\n                    mustBe == null ? \"\" : String.format(\"; you must be a %s\", joinConjunct(\" or \", mustBe)));\n        }\n\n        public String buildSecondaryString(JsonSource convert) {\n            if (entry != null) {\n                return convert.replaceText(entry);\n            }\n            return String.format(\"%s%s\",\n                    skillsToString(convert),\n                    prof == null ? \"\" : String.format(\" (%s)\", prof));\n        }\n\n        // Compensate for Lore skills..\n        // `${skill.includes(\"Lore\") ? `${renderer.render(`{@skill Lore||${skill}}`)}`\n        String skillsToString(JsonSource convert) {\n            List<String> converted = skills.stream()\n                    .map(s -> s.replaceAll(\"(.* Lore)\", \"Lore||$1\"))\n                    .map(s -> convert.linkify(Pf2eIndexType.skill, s))\n                    .collect(Collectors.toList());\n            return joinConjunct(\" or \", converted);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteSpell.java",
    "content": "package dev.ebullient.convert.tools.pf2e;\n\nimport static dev.ebullient.convert.StringUtil.isPresent;\nimport static dev.ebullient.convert.StringUtil.join;\nimport static dev.ebullient.convert.StringUtil.toOrdinal;\nimport static dev.ebullient.convert.StringUtil.toTitleCase;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map.Entry;\nimport java.util.Objects;\nimport java.util.stream.Stream;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.qute.NamedText;\nimport dev.ebullient.convert.tools.JsonNodeReader.FieldValue;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteDataDuration;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteDataRange;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteDataTimedDuration;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteSpell;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteSpell.QuteSpellAmp;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteSpell.QuteSpellDuration;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteSpell.QuteSpellSave;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteSpell.QuteSpellTarget;\nimport io.quarkus.runtime.annotations.RegisterForReflection;\n\npublic class Json2QuteSpell extends Json2QuteBase {\n    static final String SPELL_TAG = \"spell\";\n\n    public Json2QuteSpell(Pf2eIndex index, JsonNode rootNode) {\n        this(index, Pf2eIndexType.spell, rootNode);\n    }\n\n    protected Json2QuteSpell(Pf2eIndex index, Pf2eIndexType type, JsonNode rootNode) {\n        super(index, type, rootNode);\n    }\n\n    @Override\n    protected Pf2eQuteBase buildQuteResource() {\n        Tags tags = new Tags(sources);\n        List<String> text = new ArrayList<>();\n\n        appendToText(text, SourceField.entries.getFrom(rootNode), \"##\");\n\n        Collection<String> traits = collectTraitsFrom(rootNode, tags);\n\n        boolean focus = Pf2eSpell.focus.booleanOrDefault(rootNode, false);\n        String level = Pf2eSpell.level.getTextOrDefault(rootNode, \"1\");\n        String type = \"spell\";\n        if (join(\"\", traits).contains(\"cantrip\")) {\n            type = \"cantrip\";\n            tags.add(SPELL_TAG, type);\n        } else if (focus) {\n            type = \"focus\";\n            tags.add(SPELL_TAG, type, level);\n        } else {\n            tags.add(SPELL_TAG, \"level\", level);\n        }\n\n        Pf2eIndexType.domain.getListOfStrings(rootNode, tui()).forEach(d -> tags.add(\"domain\", d, \"spell\"));\n\n        // subclass --> link to subclass definition\n        NamedText.SortedBuilder namedText = new NamedText.SortedBuilder();\n        JsonNode scNode = Pf2eSpell.subclass.getFrom(rootNode);\n        if (scNode != null) {\n            for (Entry<String, JsonNode> e : iterableFields(scNode)) {\n                String[] parts = e.getKey().split(\"\\\\|\");\n                String name = parts[0];\n\n                String value = e.getValue().asText();\n                String[] vParts = value.split(\"\\\\|\");\n\n                // Construct a proper subclass link. ugh.\n                String desc = linkify(Pf2eIndexType.classtype,\n                        String.format(\"%s|%s|%s|%s\",\n                                parts[1],\n                                parts.length > 2 ? parts[2] : Pf2eIndexType.classtype.defaultSource(),\n                                vParts[0].toLowerCase(),\n                                value));\n\n                namedText.add(name, desc);\n            }\n        }\n\n        List<Pf2eSpellComponent> components = Pf2eSpell.components.getComponentsFrom(rootNode, this);\n        // Add additional traits according to present components\n        components.stream().map(Pf2eSpellComponent::getAddedTrait)\n                .distinct().map(this::linkifyTrait).forEach(traits::add);\n\n        return new QuteSpell(sources, text, tags,\n                level, toTitleCase(type),\n                traits,\n                Field.alias.replaceTextFromList(rootNode, this),\n                Pf2eSpell.cast.getDurationFrom(rootNode, this),\n                components.stream().map(c -> c.getRulesLink(this)).toList(),\n                Pf2eSpell.cost.transformTextFrom(rootNode, \", \", this),\n                Pf2eSpell.trigger.transformTextFrom(rootNode, \", \", this),\n                Pf2eSpell.requirements.transformTextFrom(rootNode, \", \", this),\n                getQuteSpellTarget(tags),\n                Pf2eSpell.savingThrow.getSpellSaveFrom(rootNode, this),\n                Pf2eSpell.duration.getSpellDurationFrom(rootNode, this),\n                Pf2eSpell.domains.linkifyListFrom(rootNode, Pf2eIndexType.domain, this),\n                Pf2eSpell.traditions.linkifyListFrom(rootNode, Pf2eIndexType.trait, this),\n                Pf2eSpell.spellLists.getListOfStrings(rootNode, tui()),\n                namedText.build(),\n                getHeightenedCast(),\n                getAmpEffects());\n    }\n\n    QuteSpellTarget getQuteSpellTarget(Tags tags) {\n        String targets = Pf2eSpell.targets.replaceTextFrom(rootNode, this);\n        SpellArea area = Pf2eSpell.area.fieldFromTo(rootNode, SpellArea.class, tui());\n        QuteDataRange range = Pf2eSpell.range.getRangeFrom(rootNode, this);\n        if (!isPresent(targets) && area == null && range == null) {\n            return null;\n        }\n        QuteSpellTarget spellTarget = new QuteSpellTarget();\n        if (isPresent(targets)) {\n            spellTarget.targets = targets;\n        }\n        spellTarget.range = range;\n        if (area != null) {\n            spellTarget.area = area.entry;\n            area.types.forEach(t -> tags.add(SPELL_TAG, \"area\", t));\n        }\n        return spellTarget;\n    }\n\n    Collection<NamedText> getHeightenedCast() {\n        JsonNode heightened = Pf2eSpell.heightened.getFrom(rootNode);\n        if (heightened == null) {\n            return null;\n        }\n        NamedText.SortedBuilder namedText = new NamedText.SortedBuilder();\n        JsonNode plusX = Pf2eSpell.plusX.getFrom(heightened);\n        JsonNode X = Pf2eSpell.X.getFrom(heightened);\n        if (plusX != null) {\n            for (var x : plusX.properties()) {\n                namedText.add(\n                        String.format(\"Heightened (+ %s)\", x.getKey()),\n                        getHeightenedValue(x.getValue()));\n            }\n        }\n        if (X != null) {\n            for (var x : X.properties()) {\n                namedText.add(\n                        String.format(\"Heightened (%s)\", toOrdinal(x.getKey())),\n                        getHeightenedValue(x.getValue()));\n            }\n        }\n        return namedText.build();\n    }\n\n    String getHeightenedValue(JsonNode value) {\n        List<String> inner = new ArrayList<>();\n        appendToText(inner, value, null);\n        return String.join(\"\\n\", inner);\n    }\n\n    QuteSpellAmp getAmpEffects() {\n        JsonNode ampNode = Pf2eSpell.amp.getFrom(rootNode);\n        if (ampNode == null) {\n            return null;\n        }\n\n        QuteSpellAmp amp = new QuteSpellAmp();\n\n        List<String> inner = new ArrayList<>();\n        appendToText(inner, SourceField.entries.getFrom(ampNode), null);\n        appendToText(inner, SourceField.entry.getFrom(ampNode), null);\n        if (!inner.isEmpty()) {\n            amp.text = String.join(\"\\n\", inner);\n        }\n\n        JsonNode heightened = Pf2eSpell.heightened.getFrom(ampNode);\n        if (heightened != null) {\n            NamedText.SortedBuilder namedText = new NamedText.SortedBuilder();\n            JsonNode plusX = Pf2eSpell.plusX.getFrom(heightened);\n            JsonNode X = Pf2eSpell.X.getFrom(heightened);\n            if (plusX != null) {\n                for (var x : plusX.properties()) {\n                    namedText.add(\n                            String.format(\"Amp Heightened (+ %s)\", x.getKey()),\n                            getHeightenedValue(x.getValue()));\n                }\n            }\n            if (X != null) {\n                for (var x : X.properties()) {\n                    namedText.add(\n                            String.format(\"Amp Heightened (%s)\", toOrdinal(x.getKey())),\n                            getHeightenedValue(x.getValue()));\n                }\n            }\n            amp.ampEffects = namedText.build();\n        }\n\n        return amp;\n    }\n\n    public enum Pf2eSpell implements Pf2eJsonNodeReader {\n        amp,\n        area,\n        basic,\n        cast,\n        components, // nested array\n        cost,\n        dismiss,\n        domains,\n        duration,\n        focus,\n        heightened,\n        hidden,\n        level,\n        plusX, // heightened\n        primaryCheck, // ritual\n        range,\n        requirements,\n        savingThrow,\n        secondaryCasters, //ritual\n        secondaryCheck, // ritual\n        spellLists,\n        subclass,\n        sustained,\n        targets,\n        traditions,\n        trigger,\n        type,\n        X; // heightened\n\n        List<Pf2eSpellComponent> getComponentsFrom(JsonNode source, JsonSource convert) {\n            if (!existsIn(source)) {\n                return List.of();\n            }\n            return getTextFrom(source)\n                    .map(Stream::of)\n                    .orElseGet(() -> streamFrom(source).flatMap(convert::streamOf).map(JsonNode::asText))\n                    .map(Pf2eSpellComponent::valueFrom).filter(Objects::nonNull)\n                    .toList();\n        }\n\n        private QuteSpellSave getSpellSaveFrom(JsonNode source, JsonSource convert) {\n            JsonNode saveNode = getFromOrEmptyObjectNode(source);\n            List<String> saves = type.getListOfStrings(saveNode, convert.tui())\n                    .stream()\n                    .map(s -> switch (s.toUpperCase().charAt(0)) {\n                        case 'F' -> \"Fortitude\";\n                        case 'R' -> \"Reflex\";\n                        case 'W' -> \"Will\";\n                        default -> null;\n                    })\n                    .filter(Objects::nonNull).toList();\n            return saves.isEmpty() ? null\n                    : new QuteSpellSave(\n                            saves, basic.booleanOrDefault(saveNode, false),\n                            hidden.booleanOrDefault(saveNode, false));\n        }\n\n        private QuteSpellDuration getSpellDurationFrom(JsonNode source, JsonSource convert) {\n            if (!isObjectIn(source)) {\n                return null;\n            }\n            JsonNode node = getFrom(source);\n            QuteDataDuration quteDuration = duration.getDurationFrom(source, convert);\n            if (quteDuration != null && quteDuration.isActivity()) {\n                convert.tui().errorf(\"Got activity as a spell duration from %s\", source.toPrettyString());\n            }\n            return new QuteSpellDuration(\n                    (QuteDataTimedDuration) quteDuration,\n                    sustained.booleanOrDefault(node, false),\n                    dismiss.booleanOrDefault(node, false));\n        }\n    }\n\n    @RegisterForReflection\n    static class SpellArea {\n        public List<String> types;\n        public String entry;\n    }\n\n    enum Pf2eSpellComponent implements FieldValue {\n        focus(\"F\", \"manipulate\"),\n        material(\"M\", \"manipulate\"),\n        somatic(\"S\", \"manipulate\"),\n        verbal(\"V\", \"concentrate\");\n\n        final String encoding;\n        final String addedTrait;\n\n        Pf2eSpellComponent(String encoding, String addedTrait) {\n            this.encoding = encoding;\n            this.addedTrait = addedTrait;\n        }\n\n        @Override\n        public String value() {\n            return encoding;\n        }\n\n        /** The trait which should be added to the spell when this component is present. */\n        public String getAddedTrait() {\n            return addedTrait;\n        }\n\n        /** Return the formatted Markdown link which explains this spell component. */\n        public String getRulesLink(JsonSource convert) {\n            return convert.createLink(\n                    name(), convert.cfg().rulesFilePath().resolve(\"core-rulebook/chapter-7-spells\"), name());\n        }\n\n        static Pf2eSpellComponent valueFrom(String value) {\n            return FieldValue.valueFrom(value, Pf2eSpellComponent.class);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteTable.java",
    "content": "package dev.ebullient.convert.tools.pf2e;\n\nimport static dev.ebullient.convert.StringUtil.join;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\n\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteNote;\n\npublic class Json2QuteTable extends Json2QuteBase {\n\n    public Json2QuteTable(Pf2eIndex index, JsonNode rootNode) {\n        super(index, Pf2eIndexType.table, rootNode);\n    }\n\n    @Override\n    protected Pf2eQuteNote buildQuteNote() {\n        Tags tags = new Tags(sources);\n        List<String> text = new ArrayList<>();\n\n        ((ObjectNode) rootNode).put(SourceField.type.name(), \"table\");\n        appendToText(text, rootNode, null);\n\n        return new Pf2eQuteNote(type, sources,\n                join(\"\\n\", text), tags);\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/Json2QuteTrait.java",
    "content": "package dev.ebullient.convert.tools.pf2e;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteNote;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteTrait;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteTraitIndex;\n\npublic class Json2QuteTrait extends Json2QuteBase {\n\n    public Json2QuteTrait(Pf2eIndex index, JsonNode rootNode) {\n        super(index, Pf2eIndexType.trait, rootNode);\n    }\n\n    @Override\n    protected QuteTrait buildQuteResource() {\n        Tags tags = new Tags(sources);\n        List<String> text = new ArrayList<>();\n        List<String> categories = new ArrayList<>();\n\n        Field.categories.getListOfStrings(rootNode, tui()).forEach(c -> {\n            tags.add(\"trait\", \"category\", c);\n\n            JsonNode implied = TraitField.implies.getFrom(rootNode);\n            if (implied != null) {\n                implied.fieldNames().forEachRemaining(n -> {\n                    if (\"spell\".equals(n.toLowerCase())) {\n                        String school = implied.get(n).get(\"_fSchool\").asText();\n                        tags.add(\"trait\", \"category\", \"spell\", school);\n                        categories.add(String.format(\"%s (%s)\", c, school));\n                    } else {\n                        tags.add(\"trait\", \"category\", n);\n                    }\n                });\n            } else {\n                categories.add(c);\n            }\n        });\n\n        appendToText(text, SourceField.entries.getFrom(rootNode), \"##\");\n\n        return new QuteTrait(sources, text, tags, List.of(), categories);\n    }\n\n    static Pf2eQuteNote buildIndex(Pf2eIndex index) {\n        Pf2eSources sources = Pf2eSources.constructSyntheticSource(\"Trait Index\");\n\n        return new QuteTraitIndex(sources, index.categoryTraitMap());\n    }\n\n    enum TraitField implements Pf2eJsonNodeReader {\n        implies,\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/JsonSource.java",
    "content": "package dev.ebullient.convert.tools.pf2e;\n\nimport static dev.ebullient.convert.StringUtil.join;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.TreeSet;\nimport java.util.stream.Collectors;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.ArrayNode;\n\nimport dev.ebullient.convert.config.TtrpgConfig;\nimport dev.ebullient.convert.io.Msg;\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.qute.QuteUtil;\nimport dev.ebullient.convert.tools.JsonNodeReader.FieldValue;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.Json2QuteAbility.Pf2eAbility;\nimport dev.ebullient.convert.tools.pf2e.Json2QuteAffliction.Pf2eAffliction;\nimport dev.ebullient.convert.tools.pf2e.Json2QuteItem.Pf2eItem;\nimport dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteDataActivity;\n\npublic interface JsonSource extends JsonTextReplacement {\n\n    /**\n     * Collect and linkify traits from the specified node.\n     *\n     * @param tags The tags to populate while collecting traits. If null, then don't populate any tags.\n     *\n     * @return an empty or sorted/linkified list of traits (never null)\n     */\n    default Set<String> collectTraitsFrom(JsonNode sourceNode, Tags tags) {\n        return Field.traits.getListOfStrings(sourceNode, tui()).stream()\n                .peek(tags == null ? t -> {\n                } : t -> tags.add(\"trait\", t))\n                .sorted()\n                .map(s -> linkify(Pf2eIndexType.trait, s))\n                .collect(Collectors.toCollection(TreeSet::new));\n    }\n\n    /**\n     * External (and recursive) entry point for content parsing.\n     *\n     * Parse attributes of the given node and add resulting lines\n     * to the provided list.\n     *\n     * @param desc Parsed content is appended to this list\n     * @param node Textual, Array, or Object node containing content to parse/render\n     * @param heading The current header depth and/or if headings are allowed for this text element\n     */\n    @Override\n    default void appendToText(List<String> text, JsonNode node, String heading) {\n        boolean pushed = parseState().push(node); // store state\n        try {\n            if (node == null || node.isNull()) {\n                // do nothing\n            } else if (node.isTextual()) {\n                text.add(replaceText(node.asText()));\n            } else if (node.isArray()) {\n                for (JsonNode f : iterableElements(node)) {\n                    maybeAddBlankLine(text);\n                    appendToText(text, f, heading);\n                }\n            } else if (node.isObject()) {\n                appendObjectToText(text, node, heading);\n            } else {\n                tui().warnf(Msg.UNKNOWN, \"Unknown entry type in %s: %s\", getSources(), node.toPrettyString());\n            }\n        } finally {\n            parseState().pop(pushed); // restore state\n        }\n    }\n\n    /** Internal */\n    default void appendObjectToText(List<String> text, JsonNode node, String heading) {\n        AppendTypeValue type = AppendTypeValue.getBlockType(node);\n        String source = SourceField.source.getTextOrEmpty(node);\n\n        // entriesOtherSource handled here.\n        if (!source.isEmpty() && !cfg().sourceIncluded(getSources())) {\n            return;\n        }\n\n        boolean pushed = parseState().push(node);\n        try {\n            if (type != null) {\n                switch (type) {\n                    case section, pf2h1, pf2h2, pf2h3, pf2h4, pf2h5 -> appendTextHeaderBlock(text, node, heading);\n                    case pf2h1flavor -> appendTextHeaderFlavorBlock(text, node);\n\n                    // callout boxes\n                    case pf2sidebar -> appendCallout(text, node, \"pf2-sidebar\");\n                    case pf2inset -> appendCallout(text, node, \"pf2-inset\");\n                    case pf2tipsBox -> appendCallout(text, node, \"pf2-tip\");\n                    case pf2sampleBox -> appendCallout(text, node, \"pf2-example\");\n                    case pf2beigeBox -> appendCallout(text, node, \"pf2-beige\");\n                    case pf2redBox -> appendCallout(text, node, \"pf2-red\");\n                    case pf2brownBox -> appendCallout(text, node, \"pf2-brown\");\n                    case pf2keyAbility -> appendCallout(text, node, \"pf2-key-ability\");\n                    case pf2keyBox -> appendCallout(text, node, \"pf2-key-box\");\n                    case pf2title -> appendTextHeaderBlock(text, node,\n                            heading == null ? null : heading + \"#\");\n\n                    // lists & items\n\n                    case pf2options, list -> appendList(text, SourceField.items.readArrayFrom(node));\n                    case item -> appendListItem(text, node);\n                    case entries -> appendToText(text, SourceField.entries.getFrom(node), heading);\n                    case table -> appendTable(text, node);\n                    case paper -> appendPaper(text, node, \"pf2-paper\");\n                    case quote -> appendQuote(text, node);\n\n                    // special inline types\n                    case ability -> appendRenderable(text, Pf2eAbility.createEmbeddedAbility(node, this));\n                    case affliction -> appendAffliction(text, node);\n                    case attack -> appendRenderable(text, Pf2eJsonNodeReader.Pf2eAttack.getAttack(node, this));\n                    case data -> embedData(text, node);\n                    case lvlEffect -> appendLevelEffect(text, node);\n                    case successDegree -> appendSuccessDegree(text, node);\n                    default -> {\n                        if (type != AppendTypeValue.entriesOtherSource) {\n                            tui().errorf(\"TODO / How did I get here?: %s %s\", type, node.toString());\n                        }\n                        appendToText(text, SourceField.entry.getFrom(node), heading);\n                        appendToText(text, SourceField.entries.getFrom(node), heading);\n                    }\n                }\n                // we had a type field! do nothing else\n                return;\n            }\n            appendToText(text, SourceField.entry.getFrom(node), heading);\n            appendToText(text, SourceField.entries.getFrom(node), heading);\n        } catch (RuntimeException ex) {\n            tui().errorf(ex, \"Error [%s] occurred while parsing %s\", ex.getMessage(), node.toString());\n            throw ex;\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    /** Internal */\n    default void appendTextHeaderBlock(List<String> text, JsonNode node, String heading) {\n        String pageRef = parseState().sourcePageString(\"<sup>%s p. %s</sup>\");\n\n        if (heading == null) {\n            List<String> inner = new ArrayList<>();\n            appendToText(inner, SourceField.entry.getFrom(node), null);\n            appendToText(inner, SourceField.entries.getFrom(node), null);\n            if (prependField(node, SourceField.name, inner)) {\n                maybeAddBlankLine(text);\n            }\n            text.addAll(inner);\n        } else if (SourceField.name.existsIn(node)) {\n            maybeAddBlankLine(text);\n            // strip rendered links from the heading: linking to headings containing links is hard\n            text.add(heading + \" \" + replaceText(SourceField.name.getTextOrEmpty(node))\n                    .replaceAll(\"\\\\[(.*?)\\\\]\\\\(.*?\\\\)\", \"$1\"));\n            text.add(pageRef);\n            appendToText(text, SourceField.entry.getFrom(node), \"#\" + heading);\n            appendToText(text, SourceField.entries.getFrom(node), \"#\" + heading);\n        } else {\n            // headers always have names, but just in case..\n            appendToText(text, SourceField.entries.getFrom(node), heading);\n        }\n    }\n\n    /** Internal */\n    default void appendTextHeaderFlavorBlock(List<String> text, JsonNode node) {\n        List<String> inner = new ArrayList<>();\n        inner.add(\"[!pf2-tip] \" + SourceField.name.getTextOrEmpty(node));\n        appendToText(inner, SourceField.entries.getFrom(node), null);\n        inner.forEach(x -> text.add(\"> \" + x));\n        maybeAddBlankLine(text);\n    }\n\n    /** Internal */\n    default void appendList(List<String> text, ArrayNode itemArray) {\n        String indent = parseState().getListIndent();\n        boolean pushed = parseState().indentList();\n        try {\n            maybeAddBlankLine(text);\n            itemArray.forEach(e -> {\n                List<String> item = new ArrayList<>();\n                appendToText(item, e, null);\n                if (item.size() > 0) {\n                    prependText(indent + \"- \", item);\n                    text.add(String.join(\"  \\n\" + indent, item));\n                }\n            });\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    /** Internal */\n    default void appendPaper(List<String> text, JsonNode paper, String callout) {\n        List<String> paperText = new ArrayList<>();\n        String title = Field.title.getTextOrDefault(paper, \"A letter\");\n\n        paperText.add(\"[!\" + callout + \"] \" + replaceText(title));\n\n        appendToText(paperText, Field.head.getFrom(paper), null);\n        maybeAddBlankLine(paperText);\n        appendToText(paperText, SourceField.entry.getFrom(paper), null);\n        maybeAddBlankLine(paperText);\n        appendToText(paperText, Field.signature.getFrom(paper), null);\n\n        maybeAddBlankLine(text);\n        paperText.forEach(x -> text.add(\"> \" + x));\n    }\n\n    /** Internal */\n    default void appendQuote(List<String> text, JsonNode entry) {\n        List<String> quoteText = new ArrayList<>();\n        String by = Field.by.getTextOrEmpty(entry);\n        if (by.isEmpty()) {\n            quoteText.add(\"[!pf2-quote]-  \");\n        } else {\n            quoteText.add(\"[!pf2-quote]- A quote from \" + replaceText(by) + \"  \");\n        }\n        appendToText(quoteText, SourceField.entry.getFrom(entry), null);\n        appendToText(quoteText, SourceField.entries.getFrom(entry), null);\n\n        String from = Field.by.getTextOrEmpty(entry);\n        if (!from.isEmpty()) {\n            maybeAddBlankLine(quoteText);\n            quoteText.add(\"-- \" + from);\n        }\n\n        maybeAddBlankLine(text);\n        quoteText.forEach(x -> text.add(\"> \" + x));\n        maybeAddBlankLine(text);\n    }\n\n    /** Internal */\n    default void appendCallout(List<String> text, JsonNode entry, String callout) {\n        List<String> insetText = new ArrayList<>();\n        String name = SourceField.name.getTextOrEmpty(entry);\n\n        insetText.add(\"[!\" + callout + \"] \" + replaceText(name));\n\n        JsonNode reference = Field.reference.getFrom(entry);\n        if (Field.auto.booleanOrDefault(reference, false)) {\n            String note = SourceField.note.getTextOrEmpty(reference);\n            if (!note.isEmpty()) {\n                insetText.add(replaceText(note));\n            } else {\n                String page = SourceField.page.getTextOrEmpty(entry);\n                insetText.add(String.format(\"See %s%s\",\n                        page == null ? \"\" : \"page \" + page + \" of \",\n                        TtrpgConfig.sourceToLongName(SourceField.source.getTextOrEmpty(entry))));\n            }\n        } else {\n            appendToText(insetText, SourceField.entry.getFrom(entry), \"##\");\n            appendToText(insetText, SourceField.entries.getFrom(entry), \"##\");\n        }\n\n        maybeAddBlankLine(text);\n        insetText.forEach(x -> text.add(\"> \" + x));\n    }\n\n    /** Internal */\n    default void appendLevelEffect(List<String> text, JsonNode node) {\n        maybeAddBlankLine(text);\n\n        SourceField.entries.streamFrom(node).forEach(e -> {\n            String range = Field.range.getTextOrEmpty(e);\n            prependTextMakeListItem(text, e, \"**\" + range + \"** \", \"    \");\n        });\n    }\n\n    /** Internal */\n    default void appendSuccessDegree(List<String> text, JsonNode node) {\n        JsonNode entries = SourceField.entries.getFrom(node);\n        String continuation = \"   \"; // properly 4, but we add space with >\n        List<String> inner = new ArrayList<>();\n        inner.add(\"[!success-degree] \");\n\n        JsonNode field = SuccessDegree.criticalSuccess.getFrom(entries);\n        if (field != null) {\n            prependTextMakeListItem(inner, field, \"**Critical Success** \", continuation);\n        }\n        field = SuccessDegree.success.getFrom(entries);\n        if (field != null) {\n            prependTextMakeListItem(inner, field, \"**Success** \", continuation);\n        }\n        field = SuccessDegree.failure.getFrom(entries);\n        if (field != null) {\n            prependTextMakeListItem(inner, field, \"**Failure** \", continuation);\n        }\n        field = SuccessDegree.criticalFailure.getFrom(entries);\n        if (field != null) {\n            prependTextMakeListItem(inner, field, \"**Critical Failure** \", continuation);\n        }\n\n        maybeAddBlankLine(text);\n        inner.forEach(x -> text.add(parseState().getListIndent()\n                + (x.isBlank() ? \">\" : \"> \")\n                + x));\n    }\n\n    /** Internal */\n    default void appendAffliction(List<String> text, JsonNode node) {\n        appendRenderable(text, Pf2eAffliction.createInlineAffliction(node, this));\n    }\n\n    /** Internal */\n    private void appendRenderable(List<String> text, QuteUtil.Renderable renderable) {\n        text.addAll(List.of(renderable.render().split(\"\\n\")));\n    }\n\n    /** Internal */\n    default void appendTable(List<String> text, JsonNode tableNode) {\n        boolean pushed = parseState().push(tableNode);\n        try {\n            List<String> table = new ArrayList<>();\n\n            String name = SourceField.name.getTextOrEmpty(tableNode);\n            String id = SourceField.id.getTextOrEmpty(tableNode);\n\n            String blockid;\n            if (TableField.spans.getFrom(tableNode) != null || tableNode.toString().contains(\"multiRow\")) {\n                blockid = appendHtmlTable(tableNode, table, id, name);\n            } else {\n                blockid = appendMarkdownTable(tableNode, table, id, name);\n            }\n\n            JsonNode intro = TableField.intro.getFrom(tableNode);\n            if (intro != null) {\n                maybeAddBlankLine(text);\n                appendToText(text, intro, null);\n            }\n            maybeAddBlankLine(text);\n\n            text.addAll(table);\n            if (!blockid.isEmpty()) {\n                table.add(\"^\" + blockid);\n            }\n            maybeAddBlankLine(text);\n            JsonNode footnotes = Field.footnotes.getFrom(tableNode);\n            if (footnotes != null) {\n                maybeAddBlankLine(text);\n                boolean pushFoot = parseState().pushFootnotes(true);\n                try {\n                    appendToText(text, footnotes, null);\n                } finally {\n                    parseState().pop(pushFoot);\n                }\n            }\n            JsonNode outro = TableField.outro.getFrom(tableNode);\n            if (outro != null) {\n                maybeAddBlankLine(text);\n                appendToText(text, outro, null);\n            }\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    /** Internal */\n    default String appendHtmlTable(JsonNode tableNode, List<String> table, String id, String name) {\n        boolean pushed = parseState().pushHtmlTable(true);\n        try {\n            ArrayNode rows = TableField.rows.readArrayFrom(tableNode);\n            JsonNode colStyles = TableField.colStyles.getFrom(tableNode);\n            int numCols = colStyles != null\n                    ? colStyles.size()\n                    : TableField.rows.streamFrom(tableNode)\n                            .map(JsonNode::size)\n                            .max(Integer::compare).get();\n\n            ArrayNode spans = TableField.spans.readArrayFrom(tableNode);\n            int spanIdx = 0;\n\n            List<Integer> labelIdx = TableField.labelRowIdx.fieldFromTo(tableNode, Tui.LIST_INT, tui());\n            if (labelIdx == null) {\n                labelIdx = List.of(0);\n            }\n\n            String blockid = slugify(id);\n            if (!name.isEmpty()) {\n                blockid = slugify(name + \" \" + id);\n            }\n\n            table.add(\"<table>\");\n            for (int r = 0; r < rows.size(); r++) {\n                JsonNode rowNode = rows.get(r);\n                int cols = rowNode.size(); // varies by row\n\n                if (AppendTypeValue.multiRow.isBlockTypeOf(rowNode)) {\n                    ArrayNode rows2 = TableField.rows.readArrayFrom(rowNode);\n                    List<List<String>> multicol = new ArrayList<>();\n                    for (int r2 = 0; r2 < rows2.size(); r2++) {\n                        ArrayNode row = (ArrayNode) rows2.get(r2);\n                        for (int c = 0; c < row.size(); c++) {\n                            if (multicol.size() <= c) {\n                                multicol.add(new ArrayList<>());\n                            }\n                            multicol.get(c).add(replaceHtmlText(row.get(c)));\n                        }\n                    }\n                    table.add(\"<tr>\");\n                    table.add(\"  <td>\"\n                            + multicol.stream()\n                                    .map(x -> String.join(\"<br />\", x))\n                                    .collect(Collectors.joining(\"</td>\\n  <td>\"))\n                            + \"</td>\");\n                    table.add(\"</tr>\");\n                } else if (cols != numCols) {\n                    String cellFormat = labelIdx.contains(r)\n                            ? \"  <th colspan=\\\"%s\\\">%s</th>\"\n                            : \"  <td colspan=\\\"%s\\\">%s</td>\";\n\n                    ArrayNode spanSizes = (ArrayNode) spans.get(spanIdx);\n                    int last = 0;\n                    table.add(\"<tr>\");\n\n                    for (int i = 0; i < cols; i++) {\n                        ArrayNode colSpan = (ArrayNode) spanSizes.get(i);\n                        JsonNode cell = rowNode.get(i);\n\n                        int start = colSpan.get(0).asInt();\n                        int end = colSpan.get(1).asInt();\n\n                        if (i == 0 && start > 1) {\n                            table.add(String.format(cellFormat, start, \"\"));\n                        } else {\n                            table.add(String.format(cellFormat,\n                                    end - last, replaceHtmlText(cell)));\n                        }\n                        last = end;\n                    }\n                    table.add(\"</tr>\");\n                    spanIdx++;\n                } else {\n                    table.add(\"<tr>\");\n                    if (labelIdx.contains(r)) {\n                        table.add(\"  <th>\" + streamOf(rowNode)\n                                .map(this::replaceHtmlText)\n                                .collect(Collectors.joining(\"</th>\\n  <th>\"))\n                                + \"</th>\");\n                    } else {\n                        table.add(\"  <td>\" + streamOf(rowNode)\n                                .map(this::replaceHtmlText)\n                                .collect(Collectors.joining(\"</td>\\n  <td>\"))\n                                + \"</td>\");\n                    }\n                    table.add(\"</tr>\");\n                }\n            }\n            table.add(\"</table>\");\n\n            return blockid;\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    /** Internal */\n    default String replaceHtmlText(JsonNode cell) {\n        return replaceText(cell.asText().trim()\n                .replaceAll(\"\\\\n\", \"<br/>\"));\n    }\n\n    /** Internal */\n    default String replaceMarkdownTableText(JsonNode cell) {\n        return replaceText(cell.asText().trim());\n    }\n\n    /** Internal */\n    default String appendMarkdownTable(JsonNode tableNode, List<String> table, String id, String name) {\n        boolean pushed = parseState().pushMarkdownTable(true);\n        try {\n            ArrayNode rows = TableField.rows.readArrayFrom(tableNode);\n            List<Integer> labelIdx = TableField.labelRowIdx.fieldFromTo(tableNode, Tui.LIST_INT, tui());\n\n            String blockid = slugify(id);\n            if (!name.isEmpty()) {\n                blockid = slugify(name + \" \" + id);\n            }\n\n            if (labelIdx == null) {\n                labelIdx = List.of(0);\n            }\n\n            for (int r = 0; r < rows.size(); r++) {\n                JsonNode rowNode = rows.get(r);\n\n                if (labelIdx.contains(r)) {\n                    String header = streamOf(rowNode)\n                            .map(x -> replaceMarkdownTableText(x))\n                            .collect(Collectors.joining(\" | \"));\n\n                    // make rollable dice headers\n                    header = \"| \" + header.replaceAll(\"^(d\\\\d+.*)\", \"dice: $1\") + \" |\";\n\n                    if (r == 0 && blockid.isBlank()) {\n                        blockid = slugify(header.replaceAll(\"d\\\\d+\", \"\")\n                                .replace(\"|\", \"\")\n                                .replaceAll(\"\\\\s+\", \" \")\n                                .trim());\n                    } else if (r != 0) {\n                        if (!blockid.isEmpty()) {\n                            table.add(\"^\" + blockid + \"-\" + r);\n                        }\n                        table.add(\"\");\n                    }\n                    table.add(header);\n                    table.add(header.replaceAll(\"[^|]\", \"-\"));\n                } else {\n                    String row = \"| \" + streamOf(rowNode)\n                            .map(x -> replaceMarkdownTableText(x))\n                            .collect(Collectors.joining(\" | \"))\n                            + \" |\";\n                    table.add(row);\n                }\n            }\n            return blockid;\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    /** Internal */\n    default void embedData(List<String> text, JsonNode dataNode) {\n        String tag = Field.tag.getTextOrEmpty(dataNode);\n        Pf2eIndexType dataType = Pf2eIndexType.fromText(tag);\n        JsonNode data = Field.data.getFrom(dataNode);\n\n        if (isEmpty(data)) {\n            // WHY?!?!\n            // {\"type\":\"data\",\"tag\":\"table\"}\n            // {\"type\":\"data\",\"tag\":\"creature\"}\n            if (SourceField.name.existsIn(dataNode)) {\n                String name = SourceField.name.getTextOrEmpty(dataNode);\n                String source = SourceField.source.getTextOrEmpty(dataNode);\n                String link = linkify(dataType, name + \"|\" + source);\n                if (dataType == Pf2eIndexType.creature) {\n                    link = link.replace(\".md)\", \".md#^statblock)\");\n                }\n                maybeAddBlankLine(text);\n                text.add(\"!\" + link);\n                maybeAddBlankLine(text);\n            }\n            return;\n        }\n\n        if (\"generic\".equals(tag)) {\n            List<String> inner = embedGenericData(tag, data);\n            maybeAddBlankLine(text);\n            wrapAdmonition(inner, \"pf2-note\");\n            text.addAll(inner);\n            return;\n        } else if (dataType == null) {\n            tui().warnf(Msg.UNKNOWN, \"Unknown data type %s from: %s\", tag, dataNode.toString());\n            return;\n        }\n\n        if (dataType == Pf2eIndexType.table) {\n            appendTable(text, data);\n            return;\n        }\n\n        // If this is a self-renderable type, then the admonition may be already included.\n        // (This might be the case anyway, but we know it probably is the case with these).\n        // So try to get the renderable embedded object first, and then add the collapsed\n        // tag to the outermost admonition.\n        QuteUtil.Renderable renderable = switch (dataType) {\n            case ability -> Pf2eAbility.createEmbeddedAbility(data, this);\n            case affliction, curse, disease -> Pf2eAffliction.createInlineAffliction(data, this);\n            default -> null;\n        };\n        if (renderable != null) {\n            List<String> renderedData = new ArrayList<>();\n            appendRenderable(renderedData, renderable);\n            // Make the outermost admonition collapsed, if there is one\n            int[] adIndices = outerAdmonitionIndices(renderedData);\n            if (adIndices != null) {\n                int adStartIdx = adIndices[0];\n                renderedData.add(adStartIdx + 1, \"collapse: closed\");\n            }\n            text.addAll(renderedData);\n            return;\n        }\n\n        // Otherwise, if it's not a self-renderable type, then we fall back to renderEmbeddedTemplate\n        // and add the collapsible admonition ourselves\n        Pf2eQuteBase converted = dataType.convertJson2QuteBase(index(), data);\n        if (converted != null) {\n            renderEmbeddedTemplate(text, converted, tag,\n                    List.of(String.format(\"title: %s\", converted.title()),\n                            \"collapse: closed\"));\n        } else {\n            tui().errorf(\"Unable to process data for %s: %s\", tag, dataNode.toString());\n        }\n    }\n\n    default List<String> embedGenericData(String tag, JsonNode data) {\n        List<String> text = new ArrayList<>();\n        boolean pushed = parseState().push(data);\n        try {\n            QuteDataActivity activity = Pf2eItem.activity.getActivityFrom(data, this);\n\n            String title = SourceField.name.getTextOrEmpty(data);\n            if (activity != null) {\n                title += \" \" + activity;\n            }\n\n            String category = Pf2eItem.category.getTextOrNull(data);\n            String level = Pf2eItem.level.getTextOrNull(data);\n            if (category != null || level != null) {\n                title += String.format(\" *%s%s%s*\",\n                        category == null ? \"\" : category,\n                        (category != null && level == null) ? \"\" : \" \",\n                        level == null ? \"\" : level);\n            }\n\n            text.add(\"title: \" + title);\n\n            // Add traits\n            Tags tags = new Tags();\n            Collection<String> traits = collectTraitsFrom(data, tags);\n            text.add(join(\"  \", traits) + \"  \");\n            maybeAddBlankLine(text);\n\n            // Add rendered sections\n            data.get(\"sections\").forEach(section -> {\n                boolean undefinedTypeText = streamOf(section)\n                        .anyMatch(x -> !x.isTextual() && !SourceField.type.existsIn(x));\n                if (undefinedTypeText) {\n                    section.forEach(x -> {\n                        if (x.isObject() && !SourceField.type.existsIn(x)) {\n                            appendToText(text, x, null);\n                        } else {\n                            appendToText(text, x, \"##\");\n                        }\n                    });\n                } else {\n                    appendToText(text, section, \"##\");\n                }\n            });\n\n            return text;\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    // Other context-constrained type values (not the big append loop)\n    enum SuccessDegree implements Pf2eJsonNodeReader {\n        criticalSuccess(\"Critical Success\"),\n        success(\"Success\"),\n        failure(\"Failure\"),\n        criticalFailure(\"Critical Failure\");\n\n        final String nodeName;\n\n        SuccessDegree(String nodeName) {\n            this.nodeName = nodeName;\n        }\n\n        public String nodeName() {\n            return nodeName;\n        }\n    }\n\n    enum TableField implements Pf2eJsonNodeReader {\n        colStyles,\n        intro,\n        labelRowIdx,\n        outro,\n        rows,\n        spans,\n    }\n\n    enum AppendTypeValue implements FieldValue {\n        ability,\n        affliction,\n        attack,\n        data,\n        entries,\n        entriesOtherSource,\n        item,\n        list,\n        lvlEffect,\n        multiRow,\n        paper,\n        pf2beigeBox(\"pf2-beige-box\"),\n        pf2brownBox(\"pf2-brown-box\"),\n        pf2h1(\"pf2-h1\"),\n        pf2h1flavor(\"pf2-h1-flavor\"),\n        pf2h2(\"pf2-h2\"),\n        pf2h3(\"pf2-h3\"),\n        pf2h4(\"pf2-h4\"),\n        pf2h5(\"pf2-h5\"),\n        pf2inset(\"pf2-inset\"),\n        pf2keyBox(\"pf2-key-box\"),\n        pf2keyAbility(\"pf2-key-ability\"),\n        pf2options(\"pf2-options\"),\n        pf2redBox(\"pf2-red-box\"),\n        pf2sampleBox(\"pf2-sample-box\"),\n        pf2sidebar(\"pf2-sidebar\"),\n        pf2tipsBox(\"pf2-tips-box\"),\n        pf2title(\"pf2-title\"),\n        quote,\n        section,\n        successDegree,\n        table;\n\n        final String nodeValue;\n\n        AppendTypeValue() {\n            nodeValue = this.name();\n        }\n\n        AppendTypeValue(String nodeValue) {\n            this.nodeValue = nodeValue;\n        }\n\n        public String value() {\n            return this.nodeValue;\n        }\n\n        /** Return the {@link AppendTypeValue} that {@code source} represents. */\n        static AppendTypeValue getBlockType(JsonNode source) {\n            return FieldValue.valueFrom(SourceField.type.getTextOrNull(source), AppendTypeValue.class);\n        }\n\n        /** Returns true if {@code node} is a block of this type. */\n        boolean isBlockTypeOf(JsonNode node) {\n            return this == getBlockType(node);\n        }\n    }\n    // enum Type\n\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/JsonTextReplacement.java",
    "content": "package dev.ebullient.convert.tools.pf2e;\n\nimport static dev.ebullient.convert.StringUtil.join;\nimport static dev.ebullient.convert.StringUtil.toAnchorTag;\nimport static dev.ebullient.convert.StringUtil.toTitleCase;\nimport static dev.ebullient.convert.StringUtil.valueOrDefault;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.regex.MatchResult;\nimport java.util.regex.Pattern;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.config.CompendiumConfig;\nimport dev.ebullient.convert.io.Msg;\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.tools.JsonNodeReader;\nimport dev.ebullient.convert.tools.JsonTextConverter;\n\npublic interface JsonTextReplacement extends JsonTextConverter<Pf2eIndexType> {\n\n    enum Field implements Pf2eJsonNodeReader {\n        alias,\n        auto,\n        by,\n        categories, // trait categories for indexing\n        customUnit,\n        data, // embedded data\n        footnotes,\n        frequency,\n        group,\n        head,\n        interval,\n        number,\n        overcharge,\n        range, // level effect\n        recurs,\n        reference,\n        requirements,\n        signature,\n        special,\n        style,\n        tag, // embedded data\n        title,\n        traits,\n        unit,\n        add_hash\n    }\n\n    Pattern asPattern = Pattern.compile(\"\\\\{@as ([^}]+)}\");\n    Pattern runeItemPattern = Pattern.compile(\"\\\\{@runeItem ([^}]+)}\");\n    Pattern chancePattern = Pattern.compile(\"\\\\{@chance ([^}]+)}\");\n    Pattern notePattern = Pattern.compile(\"\\\\{@note (\\\\*|Note:)?\\\\s?([^}]+)}\");\n    Pattern quickRefPattern = Pattern.compile(\"\\\\{@quickref ([^}]+)}\");\n\n    Pf2eIndex index();\n\n    Pf2eSources getSources();\n\n    default Tui tui() {\n        return cfg().tui();\n    }\n\n    default CompendiumConfig cfg() {\n        return index().cfg();\n    }\n\n    default String replaceText(String input) {\n        return replaceTokens(input, (s, b) -> this._replaceTokenText(s, b));\n    }\n\n    default String _replaceTokenText(String input, boolean nested) {\n        if (input == null || input.isEmpty()) {\n            return input;\n        }\n\n        try {\n            String result = input\n                    .replace(\"#$prompt_number:title=Enter Alert Level$#\", \"Alert Level\")\n                    .replace(\"#$prompt_number:title=Enter Charisma Modifier$#\", \"Charisma modifier\")\n                    .replace(\"#$prompt_number:title=Enter Lifestyle Modifier$#\", \"Charisma modifier\")\n                    .replace(\"#$prompt_number:title=Enter a Modifier$#\", \"Modifier\")\n                    .replace(\"#$prompt_number:title=Enter a Modifier,default=10$#\", \"Modifier (default 10)\")\n                    .replaceAll(\"#\\\\$prompt_number.*default=(.*)\\\\$#\", \"$1\")\n                    .replace(\"{@conditoin\", \"{@condition\")\n                    .replace(\"ffguard\", \"ff-guard\"); // fix typo;\n\n            if (parseState().inList() || parseState().inTable()) {\n                result = result.replaceAll(\"\\\\{@sup ([^}]+)}\", \"[^$1]\");\n            } else {\n                result = result.replaceAll(\"\\\\{@sup ([^}]+)}\", \"[$1]: \");\n            }\n\n            result = replaceWithDiceRoller(result); // {@hit ..} and {@d20 ..}\n\n            result = chancePattern.matcher(result)\n                    .replaceAll((match) -> match.group(1) + \"% chance\");\n\n            result = asPattern.matcher(result)\n                    .replaceAll(this::replaceActionAs);\n\n            result = quickRefPattern.matcher(result)\n                    .replaceAll((match) -> {\n                        String[] parts = match.group(1).split(\"\\\\|\");\n                        if (parts.length > 4) {\n                            return parts[4];\n                        }\n                        return parts[0];\n                    });\n\n            result = runeItemPattern.matcher(result)\n                    .replaceAll(this::linkifyRuneItem);\n\n            result = result.replaceAll(\"\\\\{@lore \", \"{@skill \");\n\n            result = Pf2eIndexType.matchPattern.matcher(result)\n                    .replaceAll(this::linkify);\n\n            // \"Style tags; {@bold some text to be bolded} (alternative {@b shorthand}),\n            // {@italic some text to be italicised} (alternative {@i shorthand}),\n            // {@underline some text to be underlined} (alternative {@u shorthand}),\n            // {@strike some text to strike-through}, (alternative {@s shorthand}),\n            // {@color color|e40707} tags, {@handwriting handwritten text},\n            // {@sup some superscript,} {@sub some subscript,}\n            // {@center some centered text} {@c with alternative shorthand,}\n            // {@nostyle to escape font formatting} {@n (see below).}}\n            // {@indentFirst You can use @indentFirst to indent the first line of text}\n            // {@indentSubsequent is the counterpart to @indentFirst. }\",\n\n            try {\n                result = result\n                        .replace(\"{@hitYourSpellAttack}\", \"the summoner's spell attack modifier\")\n                        .replaceAll(\"\\\\{@link ([^}|]+)\\\\|([^}]+)}\", \"$1 ($2)\") // this must come first\n                        .replaceAll(\"\\\\{@pf2etools ([^}|]+)\\\\|?[^}]*}\", \"$1\")\n                        .replaceAll(\"\\\\{@Pf2eTools ([^}|]+)\\\\|?[^}]*}\", \"$1\")\n                        // {@footnote directly in text|This is primarily for homebrew purposes, as the official texts (so far) avoid using footnotes},\n                        // {@footnote optional reference information|This is the footnote. References are free text.|Footnote 1, page 20}.\",\n                        .replaceAll(\"\\\\{@footnote ([^|}]+)\\\\|([^|}]+)\\\\|([^}]*)}\", \"$1 ^[$2, _$3_]\")\n                        .replaceAll(\"\\\\{@footnote ([^|}]+)\\\\|([^}]*)}\", \"$1 ^[$2]\")\n                        .replaceAll(\"\\\\{@reward ([^|}]+)\\\\|?[^}]*}\", \"$1\")\n                        .replaceAll(\"\\\\{@dc ([^}]+)}\", \"DC $1\")\n                        .replaceAll(\"\\\\{@flatDC ([^}]+)}\", \"$1\")\n                        .replaceAll(\"\\\\{@recharge ([^}]+?)}\", \"(Recharge $1-6)\")\n                        .replaceAll(\"\\\\{@recharge}\", \"(Recharge 6)\")\n                        .replaceAll(\"\\\\{@filter ([^|}]+)\\\\|?[^}]*}\", \"$1\")\n                        .replaceAll(\"\\\\{@cult ([^|}]+)\\\\|([^|}]+)\\\\|[^|}]*}\", \"$2\")\n                        .replaceAll(\"\\\\{@cult ([^|}]+)\\\\|[^}]*}\", \"$1\")\n                        .replaceAll(\"\\\\{@language ([^|}]+)\\\\|?[^}]*}\", \"$1\")\n                        .replaceAll(\"\\\\{@book ([^}|]+)\\\\|?[^}]*}\", \"\\\"$1\\\"\")\n                        .replaceAll(\"\\\\{@h}\", \"Hit: \")\n                        .replaceAll(\"\\\\{@c ([^}]+?)}\", \"$1\")\n                        .replaceAll(\"\\\\{@center ([^}]+?)}\", \"$1\")\n                        .replaceAll(\"\\\\{@s ([^}]+?)}\", \"$1\")\n                        .replaceAll(\"\\\\{@strike ([^}]+?)}\", \"$1\")\n                        .replaceAll(\"\\\\{@n ([^}]+?)}\", \"$1\")\n                        .replaceAll(\"\\\\{@b ([^}]+?)}\", \"**$1**\")\n                        .replaceAll(\"\\\\{@B ([^}]+?)}\", \"**$1**\")\n                        .replaceAll(\"\\\\{@bold ([^}]+?)}\", \"**$1**\")\n                        .replaceAll(\"\\\\{@i ([^}]+?)}\", \"_$1_\")\n                        .replaceAll(\"\\\\{@italic ([^}]+)}\", \"_$1_\")\n                        .replaceAll(\"\\\\{@indentFirst ([^}]+?)}\", \"$1\")\n                        .replaceAll(\"\\\\{@indentSubsequent ([^}]+?)}\", \"$1\");\n            } catch (Exception e) {\n                tui().errorf(e, \"Unable to parse string from %s: %s\", getSources().getKey(), input);\n            }\n\n            // second pass (nested references)\n            result = Pf2eIndexType.matchPattern.matcher(result)\n                    .replaceAll(this::linkify);\n\n            // note pattern often wraps others. Do this one last.\n            result = notePattern.matcher(result).replaceAll((match) -> {\n                if (nested) {\n                    return \"***Note:** \" + match.group(2).trim() + \"*\";\n                } else {\n                    List<String> text = new ArrayList<>();\n                    text.add(\"> [!pf2-note]\");\n                    for (String line : match.group(2).split(\"\\n\")) {\n                        text.add(\"> \" + line);\n                    }\n                    return String.join(\"\\n\", text);\n                }\n            });\n            return result;\n        } catch (IllegalArgumentException e) {\n            tui().errorf(e, \"Failure replacing text: %s\", e.getMessage());\n        }\n        return input;\n    }\n\n    default String replaceFootnoteReference(MatchResult match) {\n        return String.format(\"[^%s]%s\", match.group(1),\n                parseState().inFootnotes() ? \": \" : \"\");\n    }\n\n    default String replaceActionAs(MatchResult match) {\n        final Pf2eActivity type;\n        switch (match.group(1).toLowerCase()) {\n            case \"1\":\n            case \"a\":\n                type = Pf2eActivity.single;\n                break;\n            case \"2\":\n            case \"d\":\n                type = Pf2eActivity.two;\n                break;\n            case \"3\":\n            case \"t\":\n                type = Pf2eActivity.three;\n                break;\n            case \"f\":\n                type = Pf2eActivity.free;\n                break;\n            case \"r\":\n                type = Pf2eActivity.reaction;\n                break;\n            default:\n                type = Pf2eActivity.varies;\n                break;\n        }\n        return type.linkify(index().rulesVaultRoot());\n    }\n\n    default String linkifyRuneItem(MatchResult match) {\n        String[] parts = match.group(1).split(\"\\\\|\");\n        String linkText = parts[0];\n        // TODO {@runeItem longsword||+1 weapon potency||flaming|},\n        // {@runeItem buugeng|LOAG|+3 weapon potency||optional display text}.\n        // In general, the syntax is this:\n        // (open curly brace)@runeItem base item|base item source|rune 1|rune 1\n        // source|rune 2|rune 2 source|...|rune n|rune n source|display text(close curly\n        // brace).\n        // For each source, we assume CRB by default.\",\n\n        tui().debugf(\"TODO RuneItem found: %s\", match);\n        return linkText;\n    }\n\n    default String linkify(MatchResult match) {\n        Pf2eIndexType targetType = Pf2eIndexType.fromText(match.group(1));\n        if (targetType == null) {\n            tui().warnf(Msg.UNKNOWN, \"Unknown type to linkify: %s\", match.group(0));\n            return match.group(0);\n        }\n        return linkify(targetType, match.group(2));\n    }\n\n    default String linkify(Pf2eIndexType targetType, String match) {\n        if (match == null || match.isEmpty()) {\n            return match;\n        }\n        switch (targetType) {\n            case skill:\n                // \"Skill tags; {@skill Athletics}, {@skill Lore}, {@skill Perception}\",\n                // {@skill Lore||Farming Lore}\n                String[] parts = match.split(\"\\\\|\");\n                String linkText = valueOrDefault(parts, 2, parts[0]);\n                return linkifyRules(Pf2eIndexType.skill, linkText, \"skills\", toTitleCase(parts[0]));\n            case classtype:\n                return linkifyClass(match);\n            case classFeature:\n                return linkifyClassFeature(match);\n            case subclassFeature:\n                return linkifySubClassFeature(match);\n            case trait:\n                return linkifyTrait(match);\n            default:\n                break;\n        }\n\n        // {@action strike}\n        // {@action act together|som} can have sources added with a pipe,\n        // {@action devise a stratagem|apg|and optional link text added with another pipe}.\",\n        // {@condition stunned} assumes CRB by default,\n        // {@condition stunned|crb} can have sources added with a pipe (not that it's ever useful),\n        // {@condition stunned|crb|and optional link text added with another pipe}\n        // {@table ability modifiers} assumes CRB by default,\n        // {@table automatic bonus progression|gmg} can have sources added with a pipe,\n        // {@table domains|logm|and optional link text added with another pipe}.\",\n        String[] parts = match.split(\"\\\\|\");\n        String linkText = parts.length > 2 ? parts[2] : parts[0];\n        String source = targetType.defaultSourceString();\n\n        if (linkText.matches(\"\\\\[.+]\\\\(.+\\\\)\")) {\n            // skip if already a link\n            return linkText;\n        }\n        if (targetType == Pf2eIndexType.domain) {\n            parts[0] = parts[0].replaceAll(\"\\\\s+\\\\([Aa]pocryphal\\\\)\", \"\");\n            return linkifyRules(Pf2eIndexType.domain, linkText, \"domains\", toTitleCase(parts[0]));\n        } else if (targetType == Pf2eIndexType.condition) {\n            return linkifyRules(Pf2eIndexType.condition, linkText.replaceAll(\"\\\\s\\\\d+$\", \"\"),\n                    \"conditions\", toTitleCase(parts[0].replaceAll(\"\\\\s\\\\d+$\", \"\")));\n        }\n\n        if (parts.length > 1) {\n            source = parts[1].isBlank() ? source : parts[1];\n        }\n\n        if (targetType == Pf2eIndexType.spell) {\n            parts[0] = parts[0].replaceAll(\"\\\\s+\\\\((.*)\\\\)$\", \"-$1\");\n        }\n\n        // TODO: aliases?\n        String key = targetType.createKey(parts[0], source);\n\n        // TODO: nested file structure for some types\n        String link = String.format(\"[%s](%s/%s%s.md)\",\n                linkText,\n                targetType.relativeRepositoryRoot(index()),\n                slugify(parts[0]),\n                targetType.isDefaultSource(source) ? \"\" : \"-\" + slugify(source));\n\n        // if (targetType != Pf2eIndexType.action\n        // && targetType != Pf2eIndexType.spell\n        // && targetType != Pf2eIndexType.feat\n        // && targetType != Pf2eIndexType.trait) {\n        // tui().debugf(\"LINK for %s (%s): %s\", match, index().isIncluded(key), link);\n        // }\n        return index().isIncluded(key) ? link : linkText;\n    }\n\n    default String linkifyTrait(String match) {\n        // {@trait fire} does not require sources for official sources,\n        // {@trait brutal|b2} can have sources added with a pipe in case of homebrew or duplicate trait names,\n        // {@trait agile||and optional link text added with another pipe}.\"\n        // {@trait LN}\n\n        String[] parts = match.split(\"\\\\|\");\n        String traitName = parts[0];\n        String linkText = valueOrDefault(parts, 2, traitName);\n\n        if (parts.length < 2 && linkText.contains(\"<\")) {\n            traitName = traitName.split(\" \")[0];\n            // Get rid of angle brackets in trait names, so they don't read as HTML tags\n            if (linkText.matches(\"versatile <.*>\")) {\n                String damageType = linkText.split(\" \")[1];\n                linkText = \"versatile \" + damageType.toUpperCase();\n            }\n            linkText = linkText.replaceAll(\"<(.*)>\", \"$1\");\n        } else if (traitName.startsWith(\"[\")) {\n            // Do the same replacement we did when doing the initial import\n            // [...] becomes \"Any ...\"\n            traitName = traitName.replaceAll(\"\\\\[(.*)]\", \"Any $1\");\n        } else if (traitName.length() <= 2) {\n            Pf2eAlignmentValue alignment = Pf2eAlignmentValue.valueFrom(traitName);\n            if (alignment != null) {\n                traitName = alignment.toString();\n                // Uppercase alignment text if it's an abbreviation, e.g. \"CE\"\n                if (linkText.length() <= 2) {\n                    linkText = linkText.toUpperCase();\n                }\n            }\n            traitName = alignment == null ? traitName : alignment.toString();\n        }\n\n        String source = parts.length > 1 ? parts[1] : index().traitToSource(traitName);\n        String key = Pf2eIndexType.trait.createKey(traitName, source);\n        JsonNode traitNode = index().getIncludedNode(key);\n        return linkifyTrait(traitNode, traitName, linkText);\n    }\n\n    default String linkifyTrait(JsonNode traitNode, String traitName, String linkText) {\n        if (traitNode != null) {\n            String source = SourceField.source.getTextOrEmpty(traitNode);\n\n            return \"[%s](%s/%s%s.md \\\"%s\\\")\".formatted(\n                    linkText,\n                    Pf2eIndexType.trait.relativeRepositoryRoot(index()),\n                    slugify(traitName),\n                    Pf2eIndexType.trait.isDefaultSource(source) ? \"\" : \"-\" + slugify(source),\n                    join(\" \", traitName, traitTitle(traitNode), \"Trait\"));\n        }\n        return linkText;\n    }\n\n    /** Return the title of the trait at the given node. */\n    private String traitTitle(JsonNode traitNode) {\n        List<String> categories = Field.categories.getListOfStrings(traitNode, tui())\n                .stream()\n                .filter(x -> !\"_alignAbv\".equals(x))\n                .toList();\n\n        if (categories.contains(\"Alignment\")) {\n            return \"Alignment\";\n        } else if (categories.contains(\"Rarity\")) {\n            return \"Rarity\";\n        } else if (categories.contains(\"Size\")) {\n            return \"Size\";\n        }\n        return categories.stream().sorted().findFirst().orElse(\"\");\n    }\n\n    default String linkifyRules(Pf2eIndexType type, String text, String rules, String anchor) {\n        if (text.matches(\"\\\\[.+]\\\\(.+\\\\)\")) {\n            // skip if already a link\n            return text;\n        }\n        return String.format(\"[%s](%s/%s.md#%s)\",\n                text,\n                type.relativeRepositoryRoot(index()),\n                rules,\n                toAnchorTag(anchor));\n    }\n\n    default String linkifyClass(String match) {\n        // \"{@b Classes:}\n        // {@class alchemist} assumes CRB by default,\n        // {@class investigator|apg} can have sources added with a pipe,\n        // {@class summoner|som|optional link text added with another pipe},\n        // {@class barbarian|crb|subclasses added|giant} with another pipe,\n        // {@class barbarian|crb|and class feature added|giant|crb|2-2} with another\n        // pipe\n        // (first number is level index (0-19), second number is feature index (0-n)),\n        // although this is prone to changes in the index, it's best to use the above\n        // method instead.\",\n        String[] parts = match.split(\"\\\\|\");\n        String className = parts[0];\n        String classSource = String.valueOf(Pf2eIndexType.classtype.defaultSource());\n        String linkText = className;\n        String subclass = null;\n        if (parts.length > 3) {\n            subclass = parts[3];\n        }\n        if (parts.length > 2) {\n            linkText = parts[2];\n        }\n        if (parts.length > 1) {\n            classSource = parts[1];\n        }\n\n        return linkText;\n    }\n\n    default String linkifyClassFeature(String match) {\n        // \"{@b Class Features:}\n        // {@classFeature rage|barbarian||1},\n        // {@classFeature precise strike|swashbuckler|apg|1},\n        // {@classFeature arcane spellcasting|magus|som|1|som},\n        // {@classFeature rage|barbarian||1||optional display text}.\n        // Class source is assumed to be CRB. Class feature source is assumed to be the\n        // same as class source.\",\n        //tui().debugf(\"TODO CLASS FEATURE found: %s\", match);\n        return match;\n    }\n\n    default String linkifySubClassFeature(String match) {\n        // \"{@b Subclass Features:}\n        // {@subclassFeature research field|alchemist||bomber||1},\n        // {@subclassFeature methodology|investigator|apg|empiricism|apg|1},\n        // {@subclassFeature methodology|investigator|apg|empiricism|apg|1||and optional\n        // display text} Class and Class feature source is assumed to be CRB.\",\n        //tui().debugf(\"TODO CLASS FEATURE found: %s\", match);\n        return match;\n    }\n\n    /** Represents a PF2e alignment. */\n    enum Pf2eAlignmentValue implements JsonNodeReader.FieldValue {\n        ce(\"Chaotic Evil\"),\n        cg(\"Chaotic Good\"),\n        cn(\"Chaotic Neutral\"),\n        le(\"Lawful Evil\"),\n        lg(\"Lawful Good\"),\n        ln(\"Lawful Neutral\"),\n        n(\"Neutral\"),\n        ne(\"Neutral Evil\"),\n        ng(\"Neutral Good\");\n\n        final String longName;\n\n        Pf2eAlignmentValue(String s) {\n            longName = s;\n        }\n\n        @Override\n        public String value() {\n            return longName;\n        }\n\n        @Override\n        public String toString() {\n            return longName;\n        }\n\n        static Pf2eAlignmentValue valueFrom(String value) {\n            for (Pf2eAlignmentValue v : values()) {\n                if (v.name().equalsIgnoreCase(value)) {\n                    return v;\n                }\n            }\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eActivity.java",
    "content": "package dev.ebullient.convert.tools.pf2e;\n\nimport static dev.ebullient.convert.StringUtil.isPresent;\nimport static dev.ebullient.convert.StringUtil.join;\n\nimport java.nio.file.Path;\n\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteDataActivity;\n\npublic enum Pf2eActivity {\n    single(\"Single Action\", \">\", \"single_action.svg\"),\n    two(\"Two-Action\", \">>\", \"two_actions.svg\"),\n    three(\"Three-Action\", \">>>\", \"three_actions.svg\"),\n    free(\"Free Action\", \"F\", \"delay.svg\"),\n    reaction(\"Reaction\", \"R\", \"reaction.svg\"),\n    varies(\"Varies\", \"V\", \"load.svg\"),\n    timed(\"Duration or Frequency\", \"⏲\", \"hour-glass.svg\");\n\n    final static String DOC_PATH = \"core-rulebook/chapter-9-playing-the-game.md#Actions\";\n\n    final String longName;\n    final String markdownName;\n    final String textGlyph;\n    final String glyph;\n    final String targetFileName;\n\n    Pf2eActivity(String longName, String textGlyph, String glyph) {\n        this.longName = longName;\n        this.markdownName = longName.replace(\" \", \"%20\");\n        this.textGlyph = textGlyph;\n        this.glyph = glyph;\n\n        int x = glyph.lastIndexOf('.');\n        this.targetFileName = Tui.slugify(glyph.substring(0, x)) + glyph.substring(x);\n    }\n\n    public static Pf2eActivity toActivity(String unit, int number) {\n        switch (unit) {\n            case \"single\":\n            case \"action\":\n                switch (number) {\n                    case 1:\n                        return single;\n                    case 2:\n                        return two;\n                    case 3:\n                        return three;\n                }\n                break;\n            case \"free\":\n                return free;\n            case \"reaction\":\n                return reaction;\n            case \"varies\":\n                return varies;\n            case \"timed\":\n                return timed;\n        }\n        return null;\n    }\n\n    public String getLongName() {\n        return this.longName;\n    }\n\n    public String getTextGlyph() {\n        return this.textGlyph;\n    }\n\n    public String getGlyph() {\n        return this.glyph;\n    }\n\n    public String linkify(String rulesRoot) {\n        return String.format(\"[%s](%s \\\"%s\\\")\",\n                this.textGlyph, getRulesPath(rulesRoot), longName);\n    }\n\n    public String getRulesPath(String rulesRoot) {\n        return String.format(\"%s%s\", rulesRoot, DOC_PATH);\n    }\n\n    public QuteDataActivity toQuteActivity(JsonSource convert, String text) {\n        Path relativeTarget = Path.of(\"img\", targetFileName);\n        return new QuteDataActivity(\n                this != timed && isPresent(text) ? join(\" \", getLongName(), text) : text,\n                Pf2eSources.buildStreamImageRef(convert.index(), glyph, relativeTarget, longName),\n                textGlyph,\n                this.getRulesPath(convert.index().rulesVaultRoot()));\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eIndex.java",
    "content": "package dev.ebullient.convert.tools.pf2e;\n\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.TreeMap;\nimport java.util.TreeSet;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\n\nimport dev.ebullient.convert.config.CompendiumConfig;\nimport dev.ebullient.convert.config.TtrpgConfig;\nimport dev.ebullient.convert.io.MarkdownWriter;\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.tools.MarkdownConverter;\nimport dev.ebullient.convert.tools.ToolsIndex;\n\npublic class Pf2eIndex implements ToolsIndex, JsonSource {\n    static final String CORE_RULES_KEY = \"book|book-crb\";\n    final CompendiumConfig config;\n\n    private static final Map<String, JsonNode> imported = new HashMap<>();\n\n    private final Map<String, String> alias = new HashMap<>();\n    private final Map<String, JsonNode> filteredIndex = new TreeMap<>();\n\n    private final Map<String, String> conditionToSource = new HashMap<>();\n    private final Map<String, String> traitToSource = new HashMap<>();\n    private final Map<String, Collection<String>> categoryToTraits = new TreeMap<>();\n    private final Map<String, Set<String>> archetypeToFeats = new TreeMap<>();\n    private final Map<String, Set<String>> domainToSpells = new TreeMap<>();\n\n    final Pf2eJsonSourceCopier copier = new Pf2eJsonSourceCopier(this);\n\n    public Pf2eIndex(CompendiumConfig config) {\n        this.config = config;\n    }\n\n    @Override\n    public boolean notPrepared() {\n        return filteredIndex.isEmpty();\n    }\n\n    @Override\n    public Pf2eIndex importTree(String filename, JsonNode node) {\n        if (!node.isObject()) {\n            return this;\n        }\n\n        // user configuration\n        config.readConfigurationIfPresent(node);\n\n        // data ingest. Minimal processing.\n        Pf2eIndexType.ability.withArrayFrom(node, this::addToIndex);\n        Pf2eIndexType.action.withArrayFrom(node, this::addToIndex);\n        Pf2eIndexType.archetype.withArrayFrom(node, this::addToIndex);\n        Pf2eIndexType.background.withArrayFrom(node, this::addToIndex);\n        Pf2eIndexType.creature.withArrayFrom(node, this::addToIndex);\n        Pf2eIndexType.curse.withArrayFrom(node, this::addToIndex);\n        Pf2eIndexType.condition.withArrayFrom(node, this::addToIndex);\n        Pf2eIndexType.deity.withArrayFrom(node, this::addToIndex);\n        Pf2eIndexType.disease.withArrayFrom(node, this::addToIndex);\n        Pf2eIndexType.domain.withArrayFrom(node, this::addToIndex);\n        Pf2eIndexType.feat.withArrayFrom(node, this::addToIndex);\n        Pf2eIndexType.hazard.withArrayFrom(node, this::addToIndex);\n        Pf2eIndexType.baseitem.withArrayFrom(node, this::addToIndex);\n        Pf2eIndexType.item.withArrayFrom(node, this::addToIndex);\n        Pf2eIndexType.ritual.withArrayFrom(node, this::addToIndex);\n        Pf2eIndexType.skill.withArrayFrom(node, this::addToIndex);\n        Pf2eIndexType.spell.withArrayFrom(node, this::addToIndex);\n        Pf2eIndexType.table.withArrayFrom(node, this::addToIndex);\n        Pf2eIndexType.trait.withArrayFrom(node, this::addToIndex);\n\n        Pf2eIndexType.adventure.withArrayFrom(node, this::addToIndex);\n        Pf2eIndexType.book.withArrayFrom(node, this::addToIndex);\n\n        addDataToIndex(Pf2eIndexType.data.getFrom(node), filename);\n\n        return this;\n    }\n\n    void addToIndex(Pf2eIndexType type, JsonNode node) {\n        if (type == Pf2eIndexType.baseitem) {\n            // always use item (baseitem is a detail that we have remembered if we need it)\n            type = Pf2eIndexType.item;\n        }\n        TtrpgValue.indexInputType.setIn(node, type.name());\n        // TODO: Variants? Reprints?\n        String key = type.createKey(node);\n        String hash = Field.add_hash.getTextOrNull(node);\n        if (type == Pf2eIndexType.trait) {\n            key = prepareTrait(key, node);\n        } else if (hash != null) {\n            String name = SourceField.name.getTextOrEmpty(node);\n            name += \" (\" + hash + \")\";\n            key = replaceName(type, name, key, node, false);\n        }\n\n        if (type == Pf2eIndexType.condition) {\n            addQuickLookup(conditionToSource, node, Pf2eIndexType.condition.defaultSourceString());\n        }\n\n        if (type == Pf2eIndexType.book || type == Pf2eIndexType.adventure) {\n            String id = SourceField.id.getTextOrEmpty(node);\n            String source = SourceField.source.getTextOrEmpty(node);\n            if (!id.equals(source)) {\n                TtrpgConfig.sourceToIdMapping(source, id);\n            }\n        }\n\n        // Add the node + key to the index, and store the key in the node\n        JsonNode previous = imported.get(key);\n        if (previous != null) {\n            // We include the CRB by default, otherwise, say something about skipping duplicates\n            if (!\"book|book-crb\".equals(key) &&\n                    (!SourceField.name.valueEquals(previous, node) || !SourceField.source.valueEquals(previous, node)\n                            || !SourceField.page.valueEquals(previous, node))) {\n                tui().debugf(\"Skipping %s, already indexed\", key);\n            }\n            return;\n        }\n        imported.put(key, node);\n        TtrpgValue.indexKey.setIn(node, key);\n    }\n\n    String prepareTrait(String key, JsonNode node) {\n        String name = SourceField.name.getTextOrEmpty(node);\n        Pf2eAlignmentValue alignment = Pf2eAlignmentValue.valueFrom(SourceField.name.getTextOrNull(node));\n\n        // Change the indexed name for [...] traits\n        if (name.startsWith(\"[\") || alignment != null) {\n            // Update name & object node\n            name = alignment == null\n                    ? name.replaceAll(\"\\\\[(.*)]\", \"Any $1\")\n                    : alignment.toString();\n            key = replaceName(Pf2eIndexType.trait, name, key, node, true);\n        }\n\n        // Quick lookup for traits\n        addQuickLookup(traitToSource, node, Pf2eIndexType.trait.defaultSourceString());\n        return key;\n    }\n\n    private void addQuickLookup(Map<String, String> lookup, JsonNode node, String defaultSource) {\n        String name = SourceField.name.getTextOrEmpty(node);\n        String source = SourceField.source.getTextOrDefault(node, defaultSource);\n        String oldSource = lookup.put(name.toLowerCase(), source);\n        if (oldSource != null && !oldSource.equals(source)) {\n            tui().warnf(\"Duplicate name %s, from source %s and %s\", name, source, oldSource);\n        } else {\n            lookup.put(name.toLowerCase(), SourceField.source.getTextOrEmpty(node));\n        }\n    }\n\n    private String replaceName(Pf2eIndexType type, String newName, String oldKey, JsonNode node, boolean makeAlias) {\n        ((ObjectNode) node).put(\"name\", newName);\n\n        // Create new key, add alias from old key\n        String key = type.createKey(node);\n        if (makeAlias) {\n            alias.put(oldKey, key);\n        }\n        return key;\n    }\n\n    void addDataToIndex(JsonNode data, String filename) {\n        if (data == null || filename.isEmpty()) {\n            return;\n        }\n        int slash = filename.indexOf('/');\n        int dot = filename.indexOf('.');\n        String name = filename.substring(slash < 0 ? 0 : slash + 1, dot < 0 ? filename.length() : dot);\n        String key = Pf2eIndexType.data.createKey(name, null); // e.g. data|book-crb\n        if (imported.containsKey(key)) {\n            return;\n        }\n\n        // synthetic node\n        ObjectNode newNode = Tui.MAPPER.createObjectNode();\n        newNode.put(\"name\", name);\n        newNode.put(\"filename\", filename);\n        newNode.set(\"data\", data);\n\n        int dash = name.lastIndexOf(\"-\");\n        if (dash >= 0) {\n            newNode.put(\"source\", name.substring(dash + 1));\n        }\n        TtrpgValue.indexKey.setIn(newNode, key); // backlink\n        imported.put(key, newNode);\n    }\n\n    @Override\n    public void prepare() {\n        if (!this.filteredIndex.isEmpty()) {\n            return;\n        }\n\n        imported.forEach((key, node) -> {\n            Pf2eIndexType type = Pf2eIndexType.getTypeFromKey(key);\n\n            if (type.checkCopiesAndReprints()) {\n                // check for / manage copies first (creatures, fluff)\n                node = copier.handleCopy(type, node);\n            }\n            Pf2eSources sources = Pf2eSources.constructSources(type, node); // pre-construct sources\n\n            if (type == Pf2eIndexType.feat && keyIsIncluded(key, node)) {\n                createArchetypeReference(key, node, sources);\n            } else if (type == Pf2eIndexType.spell && keyIsIncluded(key, node)) {\n                createDomainReference(key, node);\n            } else if (type == Pf2eIndexType.trait) {\n                createTraitReference(key, node, sources);\n            }\n        });\n\n        imported.entrySet().stream()\n                .filter(e -> keyIsIncluded(e.getKey(), e.getValue()))\n                .forEach(e -> filteredIndex.put(e.getKey(), e.getValue()));\n    }\n\n    private void createTraitReference(String key, JsonNode node, Pf2eSources sources) {\n        // Precreate category mapping for traits\n        String name = SourceField.name.getTextOrEmpty(node);\n        String traitLink = linkifyTrait(node, name, name);\n\n        Field.categories.getListOfStrings(node, tui()).stream()\n                .filter(c -> !c.equalsIgnoreCase(\"_alignAbv\"))\n                .forEach(c -> categoryToTraits.computeIfAbsent(c, k -> new TreeSet<>())\n                        .add(traitLink));\n    }\n\n    void createArchetypeReference(String key, JsonNode node, Pf2eSources sources) {\n        JsonNode featType = Json2QuteFeat.Pf2eFeat.featType.getFrom(node);\n        if (featType != null) {\n            List<String> archetype = Json2QuteFeat.Pf2eFeat.archetype.getListOfStrings(featType, tui());\n            archetype.forEach(a -> {\n                String aKey = Pf2eIndexType.archetype.createKey(a, sources.primarySource());\n                archetypeToFeats.computeIfAbsent(aKey, k -> new HashSet<>())\n                        .add(key);\n            });\n        }\n    }\n\n    void createDomainReference(String key, JsonNode node) {\n        Json2QuteSpell.Pf2eSpell.domains.getListOfStrings(node, tui())\n                .forEach(d -> domainToSpells.computeIfAbsent(d.toLowerCase(), k -> new HashSet<>())\n                        .add(key));\n    }\n\n    boolean keyIsIncluded(String key, JsonNode node) {\n        Pf2eIndexType type = Pf2eIndexType.getTypeFromKey(key);\n        if (type.alwaysInclude()) {\n            return true;\n        }\n        // Check against include/exclude rules (config: included/excluded/all)\n        Optional<Boolean> rulesAllow = config.keyIsIncluded(key);\n        if (rulesAllow.isPresent()) {\n            return rulesAllow.get();\n        }\n        if (config.allSources()) {\n            return true;\n        }\n        if (CORE_RULES_KEY.equals(key)) { // include core rules unless turned off\n            return true;\n        }\n        Pf2eSources sources = Pf2eSources.findSources(key);\n        if (config.noSources()) {\n            return sources.fromDefaultSource();\n        }\n        return sources != null && sources.getSources().stream().anyMatch((s) -> config.sourceIncluded(s));\n    }\n\n    public boolean isIncluded(String key) {\n        if (filteredIndex.isEmpty()) {\n            return keyIsIncluded(key, null);\n        }\n        return filteredIndex.containsKey(aliasOrDefault(key));\n    }\n\n    // --------- Node retrieval --------\n\n    /** Used for source/page lookup during rendering */\n    public static JsonNode findNode(Pf2eSources sources) {\n        return imported.get(sources.getKey());\n    }\n\n    public String aliasOrDefault(String key) {\n        return alias.getOrDefault(key, key);\n    }\n\n    public JsonNode getIncludedNode(String key) {\n        return filteredIndex.get(aliasOrDefault(key));\n    }\n\n    public Set<String> featKeys(String archetypeKey) {\n        Set<String> feats = archetypeToFeats.get(archetypeKey);\n        return feats == null ? Set.of() : feats;\n    }\n\n    public Set<String> domainSpells(String domain) {\n        Set<String> spells = domainToSpells.get(domain.toLowerCase());\n        return spells == null ? Set.of() : spells;\n    }\n\n    public String conditionToSource(String trait) {\n        return conditionToSource.get(trait.toLowerCase());\n    }\n\n    public String traitToSource(String trait) {\n        return traitToSource.get(trait.toLowerCase());\n    }\n\n    public JsonNode getOrigin(String key) {\n        return imported.getOrDefault(key, null);\n    }\n\n    @Override\n    public JsonNode getAdventure(String a) {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    @Override\n    public JsonNode getBook(String b) {\n        // TODO Auto-generated method stub\n        return null;\n    }\n\n    // --------- Write indexes ---------\n\n    @Override\n    public MarkdownConverter markdownConverter(MarkdownWriter writer) {\n        return new Pf2eMarkdown(this, writer);\n    }\n\n    @Override\n    public void writeFullIndex(Path outputFile) throws IOException {\n        if (notPrepared()) {\n            throw new IllegalStateException(\"Index must be prepared before writing indexes\");\n        }\n        Map<String, Object> allKeys = new HashMap<>();\n        List<String> keys = new ArrayList<>(imported.keySet());\n        Collections.sort(keys);\n        allKeys.put(\"keys\", keys);\n        tui().writeJsonFile(outputFile, allKeys);\n    }\n\n    @Override\n    public void writeFilteredIndex(Path outputFile) throws IOException {\n        if (notPrepared()) {\n            throw new IllegalStateException(\"Index must be prepared before writing files\");\n        }\n        List<String> keys = new ArrayList<>(filteredIndex.keySet());\n        Collections.sort(keys);\n        tui().writeJsonFile(outputFile, Map.of(\"keys\", keys));\n    }\n\n    public Set<Map.Entry<String, JsonNode>> filteredEntries() {\n        if (notPrepared()) {\n            throw new IllegalStateException(\"Index must be prepared before writing indexes\");\n        }\n        return filteredIndex.entrySet();\n    }\n\n    public Map<String, Collection<String>> categoryTraitMap() {\n        return categoryToTraits;\n    }\n\n    // ---- JsonSource overrides ------\n\n    @Override\n    public CompendiumConfig cfg() {\n        return config;\n    }\n\n    @Override\n    public Pf2eIndex index() {\n        return this;\n    }\n\n    @Override\n    public Pf2eSources getSources() {\n        return null;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eIndexType.java",
    "content": "package dev.ebullient.convert.tools.pf2e;\n\nimport java.nio.file.Path;\nimport java.util.function.BiConsumer;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.tools.IndexType;\nimport dev.ebullient.convert.tools.JsonNodeReader;\nimport dev.ebullient.convert.tools.JsonTextConverter.SourceField;\nimport dev.ebullient.convert.tools.pf2e.Pf2eSources.DefaultSource;\nimport dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase;\n\npublic enum Pf2eIndexType implements IndexType, JsonNodeReader {\n    ability, // B1\n    action,\n    adventure,\n    affliction,\n    ancestry,\n    archetype,\n    background,\n    book,\n    classFeature,\n    classtype(\"class\"),\n    companion,\n    companionAbility,\n    condition,\n    creature, // B1\n    creatureTemplate, // B1\n    creatureTemplateFluff, // B1\n    curse(\"affliction\"), // GMG\n    data, // data from any source\n    deity,\n    deityFluff,\n    disease(\"affliction\"), // GMG\n    domain,\n    eidolon, // SoM\n    event, // LOTG\n    familiar, // APG\n    familiarAbility,\n    feat,\n    group,\n    hazard,\n    baseitem,\n    item,\n    language,\n    nation, // GMG\n    optfeature, // APG\n    organization, // LOCG\n    organizationFluff, // LOCG\n    place, // GMG\n    plane, // GMG\n    relicGift, // GMG\n    ritual,\n    settlement, // GMG\n    skill,\n    spell,\n    subclassFeature,\n    table,\n    trait,\n    trap,\n    variantrule, // GMG\n    vehicle, // GMG\n    versatileHeritage, // APG\n    syntheticGroup, // for this tool only\n    bookReference // this tool only\n    ;\n\n    final String templateName;\n\n    Pf2eIndexType() {\n        this.templateName = this.name();\n    }\n\n    Pf2eIndexType(String templateName) {\n        this.templateName = templateName;\n    }\n\n    public static final Pattern matchPattern = Pattern.compile(\"\\\\{@(\"\n            + Stream.of(values())\n                    .flatMap(x -> Stream.of(x.templateName, x.name()))\n                    .distinct()\n                    .collect(Collectors.joining(\"|\"))\n            + \") ([^{}]+?)}\");\n\n    public String templateName() {\n        return templateName;\n    }\n\n    public boolean isDefaultSource(String source) {\n        return defaultSource().sameSource(source);\n    }\n\n    public void withArrayFrom(JsonNode node, BiConsumer<Pf2eIndexType, JsonNode> callback) {\n        node.withArray(this.nodeName()).forEach(x -> callback.accept(this, x));\n    }\n\n    public static Pf2eIndexType fromText(String name) {\n        return Stream.of(values())\n                .filter(x -> x.templateName.equals(name) || x.name().equalsIgnoreCase(name))\n                .findFirst().orElse(null);\n    }\n\n    public static Pf2eIndexType getTypeFromKey(String key) {\n        String typeKey = key.substring(0, key.indexOf(\"|\"));\n        return valueOf(typeKey);\n    }\n\n    @Override\n    public String createKey(JsonNode node) {\n        if (this == book || this == adventure) {\n            String id = SourceField.id.getTextOrEmpty(node);\n            return String.format(\"%s|%s-%s\", this.name(), this.name(), id).toLowerCase();\n        }\n\n        String name = SourceField.name.getTextOrEmpty(node);\n        String source = SourceField.source.getTextOrDefault(node, this.defaultSourceString());\n        return String.format(\"%s|%s|%s\", this.name(), name, source).toLowerCase();\n    }\n\n    public String createKey(String name, String source) {\n        if (source == null || this == data) {\n            return String.format(\"%s|%s\", this.name(), name).toLowerCase();\n        }\n        return String.format(\"%s|%s|%s\", this.name(), name, source).toLowerCase();\n    }\n\n    public String getVaultRoot(Pf2eIndex index) {\n        return useCompendiumBase() ? index.compendiumVaultRoot() : index.rulesVaultRoot();\n    }\n\n    public Path getFilePath(Pf2eIndex index) {\n        return useCompendiumBase() ? index.compendiumFilePath() : index.rulesFilePath();\n    }\n\n    public String relativeRepositoryRoot(Pf2eIndex index) {\n        String root = getVaultRoot(index);\n        String relativePath = relativePath();\n\n        if (relativePath.isEmpty() || \".\".equals(relativePath)) {\n            return root.replaceAll(\"/$\", \"\");\n        }\n        return root + relativePath;\n    }\n\n    public Pf2eQuteBase convertJson2QuteBase(Pf2eIndex index, JsonNode node) {\n        // also update #isOutputType\n        return switch (this) {\n            case action -> new Json2QuteAction(index, node).build();\n            case archetype -> new Json2QuteArchetype(index, node).build();\n            case background -> new Json2QuteBackground(index, node).build();\n            case deity -> new Json2QuteDeity(index, node).build();\n            case feat -> new Json2QuteFeat(index, node).build();\n            case hazard -> new Json2QuteHazard(index, node).build();\n            case item -> new Json2QuteItem(index, node).build();\n            case ritual -> new Json2QuteRitual(index, node).build();\n            case spell -> new Json2QuteSpell(index, node).build();\n            case trait -> new Json2QuteTrait(index, node).build();\n            case creature -> new Json2QuteCreature(index, node).build();\n            default -> null;\n        };\n    }\n\n    public boolean alwaysInclude() {\n        return switch (this) {\n            case bookReference, data, syntheticGroup -> true;\n            default -> false;\n        };\n    }\n\n    public boolean checkCopiesAndReprints() {\n        return switch (this) {\n            case adventure, book, data, syntheticGroup -> false; // don't check copy/reprint fields\n            default -> true;\n        };\n    }\n\n    public boolean useQuteNote() {\n        // also update #isOutputType\n        return switch (this) {\n            case ability,\n                    affliction,\n                    book,\n                    condition,\n                    curse,\n                    disease,\n                    domain,\n                    skill,\n                    table ->\n                true; // QuteNote-based\n            default -> false;\n        };\n    }\n\n    public boolean useCompendiumBase() {\n        return switch (this) {\n            case ability,\n                    action,\n                    book,\n                    condition,\n                    trait,\n                    table,\n                    variantrule ->\n                false; // use rules\n            default -> true; // use compendium\n        };\n    }\n\n    public boolean isOutputType() {\n        return switch (this) {\n            case ability,\n                    action,\n                    affliction,\n                    archetype,\n                    background,\n                    book,\n                    condition,\n                    creature,\n                    curse,\n                    deity,\n                    disease,\n                    domain,\n                    feat,\n                    hazard,\n                    item,\n                    ritual,\n                    skill,\n                    spell,\n                    table,\n                    trait ->\n                true;\n            default -> false;\n        };\n    }\n\n    public String relativePath() {\n        // also update #isOutputType\n        return switch (this) {\n            // Simple suffix subdir (rules or compendium)\n            case action, feat, spell, table, trait, variantrule -> this.name() + 's';\n            case ritual -> \"spells/rituals\";\n            // Character\n            case ancestry -> \"character/ancestries\";\n            case classtype -> \"character/classes\";\n            case archetype, background, companion -> \"character/\" + this.name() + 's';\n            // Equipment\n            case item, vehicle -> \"equipment/\" + this.name() + 's';\n            // GM\n            case affliction, curse, disease -> \"gm/afflictions\";\n            case creature, hazard -> \"gm/\" + this.name() + 's';\n            case relicGift -> \"gm/relics-gifts\";\n            // Setting\n            case domain -> \"setting\";\n            case adventure, language, organization, place, plane, event -> \"setting/\" + this.name() + 's';\n            case deity -> \"setting/deities\";\n            case ability -> \"abilities\";\n            default -> \".\";\n        };\n    }\n\n    public String defaultSourceString() {\n        return defaultSource().name();\n    }\n\n    public DefaultSource defaultSource() {\n        return switch (this) {\n            case familiar, optfeature, versatileHeritage -> DefaultSource.apg;\n            case ability, creature, creatureTemplate -> DefaultSource.b1;\n            case action, adventure, ancestry, archetype, background, book, classFeature, classtype, companion, companionAbility,\n                    condition, deity, domain, familiarAbility, feat, group, hazard, item, language, ritual, skill, spell,\n                    subclassFeature, table, trait, trap, bookReference, syntheticGroup ->\n                DefaultSource.crb;\n            case affliction, curse, disease, nation, place, plane, relicGift, settlement, variantrule, vehicle ->\n                DefaultSource.gmg;\n            case organization -> DefaultSource.locg;\n            case event -> DefaultSource.lotg;\n            case eidolon -> DefaultSource.som;\n            default -> {\n                Tui.instance().errorf(\"Can not find defaultSource for type %s; assuming crb\", this);\n                yield DefaultSource.crb;\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonNodeReader.java",
    "content": "package dev.ebullient.convert.tools.pf2e;\n\nimport static dev.ebullient.convert.StringUtil.isPresent;\nimport static dev.ebullient.convert.StringUtil.join;\nimport static dev.ebullient.convert.StringUtil.pluralize;\nimport static dev.ebullient.convert.StringUtil.toTitleCase;\nimport static java.util.Objects.requireNonNullElse;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.function.Function;\nimport java.util.function.Predicate;\nimport java.util.stream.Collector;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.StringUtil;\nimport dev.ebullient.convert.tools.JsonNodeReader;\nimport dev.ebullient.convert.tools.pf2e.Json2QuteAbility.Pf2eAbility;\nimport dev.ebullient.convert.tools.pf2e.Json2QuteAffliction.Pf2eAffliction;\nimport dev.ebullient.convert.tools.pf2e.JsonSource.AppendTypeValue;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteAbilityOrAffliction;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteDataActivity;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteDataArmorClass;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteDataDefenses;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteDataDuration;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteDataFrequency;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteDataGenericStat;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteDataGenericStat.QuteDataNamedBonus;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteDataHpHardnessBt;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteDataRange;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteDataSpeed;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteDataTimedDuration;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteInlineAttack;\n\n/** A utility class which extends {@link JsonNodeReader} with PF2e-specific functionality. */\npublic interface Pf2eJsonNodeReader extends JsonNodeReader {\n\n    /**\n     * Return alignments as a list of formatted strings from this field in the given node.\n     * Returns an empty list if we couldn't get alignments.\n     */\n    default List<String> getAlignmentsFrom(JsonNode alignNode, JsonSource convert) {\n        return streamFrom(alignNode)\n                .map(JsonNode::asText)\n                .map(a -> a.length() > 2 ? a : convert.linkifyTrait(a.toUpperCase()))\n                .toList();\n    }\n\n    /** Return a {@link QuteDataSpeed} read from this field of the {@code source} node, or null. */\n    default QuteDataSpeed getSpeedFrom(JsonNode source, JsonSource convert) {\n        return getObjectFrom(source).map(n -> Pf2eSpeed.getSpeed(n, convert)).orElse(null);\n    }\n\n    /** Return a {@link QuteDataFrequency} read from {@code source}, or null. */\n    default QuteDataFrequency getFrequencyFrom(JsonNode source, JsonSource convert) {\n        return getObjectFrom(source).map(n -> Pf2eFrequency.getFrequency(n, convert)).orElse(null);\n    }\n\n    /** Return a {@link QuteDataDefenses} read from this field in {@code source}, or null. */\n    default QuteDataDefenses getDefensesFrom(JsonNode source, JsonSource convert) {\n        return getObjectFrom(source).map(n -> Pf2eDefenses.getDefenses(n, convert)).orElse(null);\n    }\n\n    /** Return a {@link QuteDataActivity} read from this field in {@code source}, or null */\n    default QuteDataActivity getActivityFrom(JsonNode source, JsonSource convert) {\n        return getObjectFrom(source).map(n -> Pf2eNumberUnitEntry.getActivity(n, convert)).orElse(null);\n    }\n\n    /** Return a {@link QuteDataTimedDuration} read from this field in {@code source}, or null. */\n    default QuteDataDuration getDurationFrom(JsonNode source, JsonSource convert) {\n        return getObjectFrom(source).map(n -> Pf2eNumberUnitEntry.getDuration(n, convert)).orElse(null);\n    }\n\n    /** Returns {@link QuteDataDefenses.QuteSavingThrows} read from this field in {@code source}, or null. */\n    default QuteDataDefenses.QuteSavingThrows getSavingThrowsFrom(JsonNode source, JsonSource convert) {\n        return getObjectFrom(source).map(n -> Pf2eSavingThrows.getSavingThrows(n, convert)).orElse(null);\n    }\n\n    /**\n     * Return a {@link QuteDataNamedBonus} from this field in {@code source} with a name of this field's name, or\n     * return null if this field is not present in {@code source}.\n     */\n    default QuteDataNamedBonus getNamedBonusFrom(JsonNode source, JsonSource convert) {\n        return existsIn(source) ? Pf2eNamedBonus.getNamedBonus(name(), getFrom(source), convert) : null;\n    }\n\n    /** Return a {@link QuteDataRange} from this field in {@code source}, or null. */\n    default QuteDataRange getRangeFrom(JsonNode source, JsonSource convert) {\n        return getObjectFrom(source).map(n -> Pf2eNumberUnitEntry.getRange(n, convert)).orElse(null);\n    }\n\n    /** Return a list of {@link QuteInlineAttack} from this field in {@code source}, or an empty list. */\n    default List<QuteInlineAttack> getAttacksFrom(JsonNode source, JsonSource convert) {\n        return streamFrom(source).map(n -> Pf2eAttack.getAttack(n, convert)).toList();\n    }\n\n    /**\n     * Return a list of formatted activation component strings from this field in {@code source}, and add any linked\n     * traits from these activation components to {@code traits}. Return an empty list if we couldn't get activation\n     * components.\n     */\n    default List<String> getActivationComponentsFrom(JsonNode source, Set<String> traits, JsonSource convert) {\n        List<String> rawComponents = getListOfStrings(source, convert.tui()).stream()\n                .map(s -> s.replaceFirst(\"^\\\\((%s)\\\\)$\", \"\\1\")) // remove parens\n                .toList();\n\n        // Add linked traits for the activation components to the given trait set\n        rawComponents.stream()\n                .flatMap(s -> {\n                    if (s.contains(\"envision\")) {\n                        return Stream.of(\"concentrate\");\n                    } else if (s.contains(\"command\")) {\n                        return Stream.of(\"auditory\", \"concentrate\");\n                    } else if (s.contains(\"manipulate\")) {\n                        return Stream.of(\"manipulate\");\n                    }\n                    return Stream.of();\n                }).distinct()\n                .map(convert::linkifyTrait)\n                .forEach(traits::add);\n\n        return rawComponents.stream().map(convert::replaceText).toList();\n    }\n\n    /** Return a list of {@link QuteAbilityOrAffliction} from this field in {@code source}, or an empty list. */\n    default List<QuteAbilityOrAffliction> getAbilityOrAfflictionsFrom(JsonNode source, JsonSource convert) {\n        return streamFrom(source)\n                .map(n -> switch (requireNonNullElse(AppendTypeValue.getBlockType(n), AppendTypeValue.ability)) {\n                    case affliction -> (QuteAbilityOrAffliction) Pf2eAffliction.createInlineAffliction(n, convert);\n                    case ability -> Pf2eAbility.createEmbeddedAbility(n, convert);\n                    default -> {\n                        convert.tui().debugf(\"Unexpected block type in %s\", source.toPrettyString());\n                        yield null;\n                    }\n                })\n                .filter(Objects::nonNull)\n                .toList();\n    }\n\n    /**\n     * A {@link Pf2eJsonNodeReader} which reads JSON like the following:\n     *\n     * <pre>\n     *     {\n     *         \"walk\": 10,\n     *         \"fly\": 20,\n     *         \"speedNote\": \"(with fly spell)\",\n     *         \"abilities\": \"air walk\"\n     *     }\n     * </pre>\n     */\n    enum Pf2eSpeed implements Pf2eJsonNodeReader {\n        walk,\n        speedNote,\n        abilities;\n\n        /** Read a {@link QuteDataSpeed} from the {@code source} node. */\n        private static QuteDataSpeed getSpeed(JsonNode source, JsonSource convert) {\n            return new QuteDataSpeed(\n                    walk.intOrNull(source),\n                    convert.streamPropsExcluding(source, speedNote, abilities)\n                            .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().asInt())),\n                    speedNote.getTextFrom(source)\n                            .map(convert::replaceText)\n                            // Remove parens around the note\n                            .map(s -> s.replaceFirst(\"^\\\\((%s)\\\\)$\", \"\\1\"))\n                            .map(List::of).orElse(List.of()),\n                    // Specifically make this mutable because we later need to add additional abilities for deities\n                    new ArrayList<>(abilities.replaceTextFromList(source, convert)));\n        }\n    }\n\n    /**\n     * A {@link Pf2eJsonNodeReader} which reads JSON like the following:\n     *\n     * <pre>\n     *     \"unit\": \"round\",\n     *     \"number\": 1,\n     *     \"recurs\": true,\n     *     \"overcharge\": true,\n     *     \"interval\": 2,\n     * </pre>\n     *\n     * <p>\n     * Or, with a custom unit:\n     * </p>\n     *\n     * <pre>\n     *     \"customUnit\": \"gem\",\n     *     \"number\": 1,\n     *     \"recurs\": true,\n     *     \"overcharge\": true,\n     *     \"interval\": 2,\n     * </pre>\n     *\n     * <p>\n     * Or, for a special frequency with a custom string:\n     * </p>\n     *\n     * <pre>\n     *     \"special\": \"once per day, and recharges when the great cyclops uses Ferocity\"\n     * </pre>\n     */\n    enum Pf2eFrequency implements Pf2eJsonNodeReader {\n        special,\n        number,\n        recurs,\n        overcharge,\n        interval,\n        unit,\n        customUnit;\n\n        /** Return a {@link QuteDataFrequency} read from {@code node}. */\n        private static QuteDataFrequency getFrequency(JsonNode node, JsonSource convert) {\n            if (special.getTextFrom(node).isPresent()) {\n                return new QuteDataFrequency(special.replaceTextFrom(node, convert));\n            }\n            return new QuteDataFrequency(\n                    // This should usually be an integer, but some entries deviate from the schema and use a word\n                    number.intFrom(node).orElseGet(() -> {\n                        // Try to coerce the word back into a number, and otherwise log an error and give 0\n                        String freqString = number.getTextOrThrow(node).trim();\n                        if (freqString.equalsIgnoreCase(\"once\")) {\n                            return 1;\n                        }\n                        convert.tui().errorf(\"Got unexpected frequency value \\\"%s\\\"\", freqString);\n                        return 0;\n                    }),\n                    interval.intOrNull(node),\n                    unit.getTextFrom(node).orElseGet(() -> customUnit.getTextOrThrow(node)),\n                    recurs.booleanOrDefault(node, false),\n                    overcharge.booleanOrDefault(node, false));\n        }\n    }\n\n    /** Read {@link QuteDataDefenses} from JSON input data. */\n    enum Pf2eDefenses implements Pf2eJsonNodeReader {\n        abilities,\n        ac,\n        hardness,\n        hp,\n        bt,\n        immunities,\n        note,\n        notes,\n        resistances,\n        savingThrows,\n        std,\n        weaknesses;\n\n        /**\n         * Read {@link QuteDataDefenses} from {@code source}. Example JSON input:\n         *\n         * <pre>\n         *     \"ac\": {\n         *         \"std\": 14,\n         *         \"with mage armor\": 16,\n         *         \"notes\": [\"another armor note\"],\n         *         \"abilities\": [\"some armor ability\"]\n         *     },\n         *     \"savingThrows\": { ... },\n         *     \"hp\": { ... },\n         *     \"hardness\": { ... },\n         *     \"bt\": { ... },\n         *     \"immunities\": [\n         *         \"critical hits\",\n         *         \"precision damage\"\n         *     ],\n         *     \"resistances\": [ ... ],\n         *     \"weaknesses\": [ ... ],\n         *     \"notes\": { \"some key\": \"some value\" }\n         * </pre>\n         */\n        private static QuteDataDefenses getDefenses(JsonNode source, JsonSource convert) {\n            if (notes.existsIn(source)) {\n                convert.tui().warnf(\"Defenses has notes: %s\", source.toString());\n            }\n\n            Map<String, QuteDataHpHardnessBt> hpHardnessBt = getHpHardnessBt(source, convert);\n\n            List<String> immunityLinks = Pf2eDefenses.immunities.getListOfStrings(source, convert.tui())\n                    .stream()\n                    .map(s -> {\n                        if (convert.index().traitToSource(s) != null) {\n                            return convert.linkify(Pf2eIndexType.trait, s);\n                        } else if (convert.index().conditionToSource(s) != null) {\n                            return convert.linkify(Pf2eIndexType.condition, s);\n                        } else {\n                            return convert.replaceText(s);\n                        }\n                    })\n                    .toList();\n\n            return new QuteDataDefenses(\n                    ac.getObjectFrom(source)\n                            .map(acNode -> new QuteDataArmorClass(\n                                    std.intOrThrow(acNode),\n                                    ac.streamPropsExcluding(source, note, abilities, notes, std)\n                                            .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().asInt())),\n                                    (note.existsIn(acNode) ? note : notes).replaceTextFromList(acNode, convert),\n                                    abilities.replaceTextFromList(acNode, convert)))\n                            .orElse(null),\n                    savingThrows.getSavingThrowsFrom(source, convert),\n                    hpHardnessBt.remove(std.name()),\n                    hpHardnessBt,\n                    immunityLinks,\n                    resistances.streamFrom(source).collect(Pf2eNameAmountNote.mappedStatCollector(convert)),\n                    weaknesses.streamFrom(source).collect(Pf2eNameAmountNote.mappedStatCollector(convert)));\n        }\n\n        /**\n         * Returns a map of names to {@link QuteDataHpHardnessBt} read from {@code source}. This can read data from\n         * creatures or from hazards.\n         *\n         * <p>\n         * Example input JSON for a hazard:\n         * </p>\n         *\n         * <pre>\n         *     \"hardness\": {\n         *         \"std\": 13,\n         *         \"Reflection \": 14,\n         *         \"notes\": {\n         *             \"std\": \"per mirror\"\n         *         }\n         *     },\n         *     \"hp\": {\n         *         \"std\": 54,\n         *         \"Reflection \": 30,\n         *         \"notes\": {\n         *             \"Reflection \": \"some note\"\n         *         }\n         *     },\n         *     \"bt\": {\n         *         \"std\": 27,\n         *         \"Reflection \": 15\n         *     }\n         * </pre>\n         *\n         * <p>\n         * Broken threshold is only valid for hazards. Example input JSON for a creature:\n         * </p>\n         *\n         * <pre>\n         *     \"hardness\": {\n         *         \"std\": 13,\n         *     },\n         *     \"hp\": [\n         *         { \"hp\": 90, \"name\": \"body\", \"abilities\": [ \"hydra regeneration\" ] },\n         *         { \"hp\": 15, \"name\": \"head\", \"abilities\": [ \"head regrowth\" ] }\n         *     ],\n         * </pre>\n         */\n        private static Map<String, QuteDataHpHardnessBt> getHpHardnessBt(JsonNode source, JsonSource convert) {\n            Map<String, QuteDataHpHardnessBt.HpStat> hpStats = hp.getHpFrom(source, convert);\n            JsonNode btNode = bt.getFromOrEmptyObjectNode(source);\n            JsonNode hardnessNode = hardness.getFromOrEmptyObjectNode(source);\n            Map<String, String> hardnessNotes = notes.getMapOfStrings(hardnessNode, convert.tui());\n            // Collect distinct names from the field names of the stat objects\n            Stream<String> names = Stream.of(\n                    hpStats.keySet().stream(),\n                    bt.streamPropsExcluding(source, abilities, notes).map(Map.Entry::getKey),\n                    hardness.streamPropsExcluding(source, abilities, notes).map(Map.Entry::getKey))\n                    .flatMap(Function.identity())\n                    .distinct();\n            // Map each known name to the known stats for that name\n            return names.collect(Collectors.toMap(\n                    String::trim,\n                    k -> new QuteDataHpHardnessBt(\n                            hpStats.getOrDefault(k, null),\n                            hardnessNode.has(k)\n                                    ? new QuteDataGenericStat.SimpleStat(\n                                            hardnessNode.get(k).asInt(), convert.replaceText(hardnessNotes.get(k)))\n                                    : null,\n                            btNode.has(k) ? btNode.get(k).asInt() : null)));\n        }\n\n        /**\n         * Returns a map of names to {@link QuteDataHpHardnessBt.HpStat} from this field of {@code source}, or an empty\n         * map.\n         */\n        private Map<String, QuteDataHpHardnessBt.HpStat> getHpFrom(JsonNode source, JsonSource convert) {\n            // We need to do HP mapping separately because creature and hazard HP are structured differently\n            return isArrayIn(source)\n                    ? Pf2eHpStat.getHpMapFromArray(ensureArrayIn(source), convert)\n                    : Pf2eHpStat.getHpMapFromObject(getFromOrEmptyObjectNode(source), convert);\n        }\n\n    }\n\n    /** A {@link Pf2eJsonNodeReader} which reads JSON HP data. */\n    enum Pf2eHpStat implements Pf2eJsonNodeReader {\n        abilities,\n        hp,\n        name,\n        notes,\n        std;\n\n        /**\n         * Read HP stats mapped to names from a JSON array. Each entry in the array corresponds to a different HP\n         * component in a single creature - e.g. the heads and body on a hydra:\n         *\n         * <pre>\n         *     [\n         *       {\"hp\": 10, \"name\": \"head\", \"abilities\": [\"head regrowth\"]},\n         *       {\"hp\": 20, \"name\": \"body\", \"notes\": [\"some note\"]}\n         *     ]\n         * </pre>\n         *\n         * If there is only a single HP component, then {@code name} may be omitted. In this case, the key in the map\n         * will be {@code std}.\n         *\n         * <pre>\n         *     [{\"hp\": 10, \"abilities\": [\"some ability\"], \"notes\": [\"some note\"]}]\n         * </pre>\n         */\n        private static Map<String, QuteDataHpHardnessBt.HpStat> getHpMapFromArray(\n                JsonNode source, JsonSource convert) {\n            return convert.streamOf(convert.ensureArray(source)).collect(Collectors.toMap(\n                    n -> Pf2eHpStat.name.getTextFrom(n).map(StringUtil::toTitleCase).orElse(std.name()),\n                    n -> new QuteDataHpHardnessBt.HpStat(\n                            hp.intOrThrow(n),\n                            notes.replaceTextFromList(n, convert),\n                            abilities.replaceTextFromList(n, convert))));\n        }\n\n        /**\n         * Read HP stats mapped to names from a JSON object. Each resulting entry corresponds to a different HP\n         * component in a single hazard - e.g. the standard HP, and the reflection HP on a Clone Mirror hazard:\n         *\n         * <pre>\n         *     \"std\": 54,\n         *     \"Reflection \": 30,\n         *     \"notes\": {\n         *         \"std\": \"per mirror\"\n         *     }\n         * </pre>\n         */\n        private static Map<String, QuteDataHpHardnessBt.HpStat> getHpMapFromObject(\n                JsonNode source, JsonSource convert) {\n            Map<String, String> noteMap = notes.getMapOfStrings(source, convert.tui());\n            return convert.streamPropsExcluding(convert.ensureObjectNode(source), notes).collect(Collectors.toMap(\n                    Map.Entry::getKey,\n                    e -> new QuteDataHpHardnessBt.HpStat(\n                            e.getValue().asInt(),\n                            convert.replaceText(noteMap.get(e.getKey())))));\n        }\n    }\n\n    /**\n     * A {@link Pf2eJsonNodeReader} which reads JSON structured like the following:\n     *\n     * <pre>\n     *     \"name\": \"physical\",\n     *     \"amount\": 5,\n     *     \"note\": \"except bludgeoning\"\n     * </pre>\n     */\n    enum Pf2eNameAmountNote implements Pf2eJsonNodeReader {\n        name,\n        amount,\n        note;\n\n        /** Return a collector which returns a map of names to {@link QuteDataGenericStat} */\n        private static Collector<JsonNode, ?, Map<String, QuteDataGenericStat>> mappedStatCollector(\n                JsonSource convert) {\n            return Collectors.toMap(\n                    n -> name.replaceTextFrom(n, convert),\n                    n -> new QuteDataGenericStat.SimpleStat(\n                            amount.intOrNull(n),\n                            note.replaceTextFrom(n, convert)));\n        }\n    }\n\n    /**\n     * A {@link Pf2eJsonNodeReader} which reads JSON input structured like the following:\n     *\n     * <pre>\n     *     \"number\": 1,\n     *     \"unit\": \"round\",\n     *     \"entry\": \"until the start of your next turn\"\n     * </pre>\n     *\n     * Or, possibly for some range entries\n     *\n     * <pre>\n     *     \"entry\": \"10 miles\",\n     *     \"distance\": {\"type\": \"mile\", \"amount\": 10}\n     * </pre>\n     */\n    enum Pf2eNumberUnitEntry implements Pf2eJsonNodeReader {\n        number,\n        unit,\n        entry,\n        distance,\n        type,\n        amount;\n\n        /**\n         * Return a {@link QuteDataActivity} read from {@code node}. Example JSON input:\n         *\n         * <pre>\n         *     {\"number\": 1, \"unit\": \"action\"}\n         * </pre>\n         *\n         * Or, for an activity with a custom entry:\n         *\n         * <pre>\n         *     {\"number\": 1, \"unit\": \"varies\", \"entry\": \"{&#64;as 3} command,\"}\n         * </pre>\n         */\n        private static QuteDataActivity getActivity(JsonNode node, JsonSource convert) {\n            String actionType = unit.getTextOrNull(node);\n            Pf2eActivity activity = switch (actionType) {\n                case \"single\", \"action\", \"free\", \"reaction\" ->\n                    Pf2eActivity.toActivity(actionType, number.intOrThrow(node));\n                case \"varies\" -> Pf2eActivity.varies;\n                case \"day\", \"minute\", \"hour\", \"round\" -> Pf2eActivity.timed;\n                default -> null;\n            };\n\n            if (activity == null) {\n                throw new IllegalArgumentException(\"Can't parse activity from: %s\".formatted(node));\n            }\n\n            String extra = entry.getTextFrom(node)\n                    .filter(s -> !s.toLowerCase().contains(\"varies\"))\n                    .filter(Predicate.not(String::isBlank))\n                    .map(convert::replaceText).map(StringUtil::parenthesize)\n                    .orElse(\"\");\n\n            return activity.toQuteActivity(\n                    convert, activity == Pf2eActivity.timed ? join(\" \", number.intOrThrow(node), actionType, extra) : extra);\n        }\n\n        /**\n         * Return a {@link QuteDataDuration} read from {@code node}. This will be either a {@link QuteDataActivity},\n         * or a {@link QuteDataTimedDuration}. Examples:\n         *\n         * <ul>\n         * <li>{@code {\"entry\": \"1 round or 1 minute\"}} timed duration with custom display of \"1 round or 1 minute\"</li>\n         * <li>{@code {\"number\": 1, \"unit\": \"action\"}} single-action activity</li>\n         * <li>{@code {\"number\": 1, \"unit\": \"day\"}} timed duration with value 1, unit \"day\"</li>\n         * </ul>\n         */\n        private static QuteDataDuration getDuration(JsonNode node, JsonSource convert) {\n            QuteDataTimedDuration timedDuration = new QuteDataTimedDuration(\n                    number.intFrom(node).orElse(1),\n                    unit.getEnumValueFrom(node, QuteDataTimedDuration.DurationUnit.class),\n                    entry.replaceTextFrom(node, convert));\n            // Prioritize using a custom display string if we have one\n            if (timedDuration.hasCustomDisplay()) {\n                return timedDuration;\n            }\n            String unitText = unit.getTextOrNull(node);\n            // This is disallowed by the schema, but if we don't get a unit, then discard the whole duration\n            if (unitText == null) {\n                return null;\n            }\n            // The activity is more specific unless we have a custom display. Otherwise, fall back to the timed duration\n            return Optional.ofNullable(Pf2eActivity.toActivity(unitText, timedDuration.value()))\n                    .map(a -> (QuteDataDuration) a.toQuteActivity(convert, null))\n                    .orElse(timedDuration);\n        }\n\n        /**\n         * Return a {@link QuteDataRange} read from {@code node}. Example JSON input:\n         *\n         * <pre>\n         *     \"number\": 3,\n         *     \"unit\": \"mile\",\n         *     \"entry\": \"some note\"\n         * </pre>\n         *\n         * Or\n         *\n         * <pre>\n         *     \"entry\": \"10 miles\",\n         *     \"distance\": {\"type\": \"mile\", \"amount\": 10}\n         * </pre>\n         */\n        private static QuteDataRange getRange(JsonNode source, JsonSource convert) {\n            // Sometimes the amount and unit are in different fields of a nested \"distance\" object\n            JsonNode node = distance.getObjectFrom(source).orElse(source);\n            QuteDataRange.RangeUnit rangeUnit = unit.getRangeUnitFrom(node)\n                    .or(() -> type.getRangeUnitFrom(node))\n                    .or(() -> entry.getRangeUnitFrom(node)) // sometimes the entry is the unit\n                    .orElse(null);\n            Integer rangeValue = number.intFrom(node)\n                    .or(() -> amount.intFrom(node))\n                    .orElse(null);\n            String entryText = entry.replaceTextFrom(node, convert);\n            if (rangeValue == null && rangeUnit == null && !isPresent(entryText)) {\n                convert.tui().errorf(\"No range data in %s\", source.toPrettyString());\n            }\n            return new QuteDataRange(\n                    rangeValue, rangeUnit,\n                    // Don't include the entry text if it's just the unit again\n                    entry.getRangeUnitFrom(node).map(ru -> ru == rangeUnit).orElse(false) ? null : entryText);\n        }\n\n        private Optional<QuteDataRange.RangeUnit> getRangeUnitFrom(JsonNode source) {\n            return getTextFrom(source)\n                    // normalize the unit to match enum names\n                    .map(s -> pluralize(s, 1).toUpperCase())\n                    .map(s -> {\n                        try {\n                            return QuteDataRange.RangeUnit.valueOf(s);\n                        } catch (IllegalArgumentException ignored) {\n                            return null;\n                        }\n                    });\n        }\n    }\n\n    /**\n     * Example JSON input for a creature:\n     *\n     * <pre>\n     *     \"range\": \"Melee\",\n     *     \"name\": \"jaws\",\n     *     \"attack\": 32,\n     *     \"traits\": [\"evil\", \"magical\", \"reach 10 feet\"],\n     *     \"effects\": [\"essence drain\", \"Grab\"],\n     *     \"damage\": \"3d8+9 piercing plus 1d6 evil, essence drain, and Grab\",\n     *     \"types\": [\"evil\", \"piercing\"]\n     * </pre>\n     *\n     * An example for a hazard with a complicated effect:\n     *\n     * <pre>\n     *     \"type\": \"attack\",\n     *     \"range\": \"Ranged\",\n     *     \"name\": \"eye beam\",\n     *     \"attack\": 20,\n     *     \"traits\": [\"diving\", \"evocation\", \"range 120 feet\"],\n     *     \"effects\": [\n     *         \"The target is subjected to one of the effects summarized below.\",\n     *         {\n     *             \"type\": \"list\",\n     *             \"items\": [{\n     *                 \"type\": \"item\",\n     *                 \"name\": \"Green Eye Beam\",\n     *                 \"entries\": [\"(poison) 6d6 poison damage (DC24 basic Reflex save)\"],\n     *             }, ...],\n     *         },\n     *     ],\n     *     \"types\": [\"electricity\", \"fire\", \"poison\", \"acid\"]\n     * </pre>\n     *\n     */\n    enum Pf2eAttack implements Pf2eJsonNodeReader {\n        name,\n        attack,\n        activity,\n        damage,\n        effects,\n        range,\n        types,\n        noMAP;\n\n        /** Returns a {@link QuteInlineAttack} read from {@code node} */\n        public static QuteInlineAttack getAttack(JsonNode node, JsonSource convert) {\n            List<String> attackEffects = Pf2eAttack.effects.transformListFrom(node, convert);\n            // Either the effects are a list of short descriptors which are also included in the damage, or they are a\n            // long multi-line description of a complicated effect.\n            String formattedDamage = damage.replaceTextFrom(node, convert);\n            boolean hasMultilineEffect = attackEffects.stream().anyMatch(Predicate.not(formattedDamage::contains));\n\n            return new QuteInlineAttack(\n                    name.replaceTextFrom(node, convert),\n                    Optional.ofNullable(activity.getActivityFrom(node, convert))\n                            .orElse(Pf2eActivity.single.toQuteActivity(convert, \"\")),\n                    QuteInlineAttack.AttackRangeType.valueOf(range.getTextOrDefault(node, \"Melee\").toUpperCase()),\n                    attack.intOrNull(node),\n                    formattedDamage,\n                    types.replaceTextFromList(node, convert),\n                    convert.collectTraitsFrom(node, null),\n                    hasMultilineEffect ? List.of() : attackEffects,\n                    hasMultilineEffect ? String.join(\"\\n\", attackEffects) : null,\n                    noMAP.booleanOrDefault(node, false) ? List.of() : List.of(\"no multiple attack penalty\"),\n                    convert);\n        }\n    }\n\n    /**\n     * A {@link Pf2eJsonNodeReader} which parses JSON named bonuses. Example:\n     *\n     * <pre>\n     *     \"std\": 10,\n     *     \"in woods\": 12,\n     *     \"note\": \"some note\"\n     * </pre>\n     */\n    enum Pf2eNamedBonus implements Pf2eJsonNodeReader {\n        std,\n        note,\n        notes,\n        abilities;\n\n        /**\n         * Return a {@link QuteDataNamedBonus} read from {@code source} with the given {@code skillName}. If\n         * {@code source} is an integer, then return a bonus of that amount. If {@code source} is null, then return a\n         * bonus of 0.\n         *\n         * <p>\n         * Reads either single note from {@code note}, or multiple notes from {@code abilities}.\n         * </p>\n         */\n        public static QuteDataNamedBonus getNamedBonus(\n                String skillName, JsonNode source, JsonSource convert) {\n            String displayName = toTitleCase(skillName);\n\n            if (source == null) {\n                return new QuteDataNamedBonus(displayName, 0);\n            }\n\n            if (source.isInt()) {\n                return new QuteDataNamedBonus(displayName, source.asInt());\n            }\n\n            return new QuteDataNamedBonus(\n                    displayName,\n                    std.intOrThrow(source),\n                    convert.streamPropsExcluding(source, std, note)\n                            .collect(Collectors.toMap(e -> convert.replaceText(e.getKey()), e -> e.getValue().asInt())),\n                    note.getTextFrom(source).map(convert::replaceText).map(List::of)\n                            .orElse((abilities.existsIn(source) ? abilities : notes).replaceTextFromList(source, convert)));\n        }\n    }\n\n    /**\n     * A {@link Pf2eJsonNodeReader} which reads JSON saving throw data. Example:\n     *\n     * <pre>\n     *     \"fort\": {\n     *         \"std\": 6,\n     *         \"vs. poisons\": +8\n     *         \"abilities\": [\"some fort ability\"]\n     *     },\n     *     \"ref\": { ... },\n     *     \"will\": { ... },\n     *     \"abilities\": [\"+1 status to all saves vs. positive\"]\n     * </pre>\n     */\n    enum Pf2eSavingThrows implements Pf2eJsonNodeReader {\n        fort,\n        ref,\n        will,\n        abilities,\n        std;\n\n        /** Return {@link QuteDataDefenses.QuteSavingThrows} read from this field in {@code source}. */\n        private static QuteDataDefenses.QuteSavingThrows getSavingThrows(JsonNode source, JsonSource convert) {\n            return new QuteDataDefenses.QuteSavingThrows(\n                    fort.getNamedBonusFrom(source, convert),\n                    ref.getNamedBonusFrom(source, convert),\n                    will.getNamedBonusFrom(source, convert),\n                    abilities.replaceTextFromList(source, convert));\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonSourceCopier.java",
    "content": "package dev.ebullient.convert.tools.pf2e;\n\nimport java.util.List;\nimport java.util.Set;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport com.fasterxml.jackson.databind.node.TextNode;\n\nimport dev.ebullient.convert.tools.JsonSourceCopier;\n\npublic class Pf2eJsonSourceCopier extends JsonSourceCopier<Pf2eIndexType> implements JsonSource {\n    private static final List<String> COPY_ENTRY_PROPS = List.of(\n            \"attacks\", \"abilities.top\", \"abilities.mid\", \"abilities.bot\");\n    private static final Set<String> MERGE_PRESERVE_BASE = Set.of(\"page\", \"otherSources\");\n    private static final Set<String> MERGE_PRESERVE_CREATURE = Set.of(\n            \"page\", \"otherSources\", \"hasImages\", \"description\");\n\n    final Pf2eIndex index;\n\n    Pf2eJsonSourceCopier(Pf2eIndex index) {\n        this.index = index;\n    }\n\n    @Override\n    public Pf2eIndex index() {\n        return index;\n    }\n\n    @Override\n    public Pf2eSources getSources() {\n        throw new IllegalStateException(\"Should not call getSources while copying source\");\n    }\n\n    @Override\n    protected JsonNode getOriginNode(String key) {\n        return index.getOrigin(key);\n    }\n\n    @Override\n    protected boolean mergePreserveKey(Pf2eIndexType type, String key) {\n        return switch (type) {\n            case ritual, optfeature, spell, background,\n                    deity, deityFluff, organization, organizationFluff,\n                    creatureTemplate, creatureTemplateFluff ->\n                MERGE_PRESERVE_BASE.contains(key);\n            case creature -> MERGE_PRESERVE_CREATURE.contains(key);\n            default -> false;\n        };\n    }\n\n    @Override\n    protected List<String> getCopyEntryProps() {\n        return COPY_ENTRY_PROPS;\n    }\n\n    @Override\n    protected JsonNode resolveDynamicVariable(\n            String originKey, JsonNode value, JsonNode target, TemplateVariable variableMode, String[] params) {\n        return variableMode == TemplateVariable.name\n                ? new TextNode(SourceField.name.getTextOrEmpty(target))\n                : value;\n    }\n\n    @Override\n    protected JsonNode mergeNodes(Pf2eIndexType type, String originKey, JsonNode copyFrom, ObjectNode target) {\n        JsonNode _copy = MetaFields._copy.getFromOrEmptyObjectNode(target);\n        normalizeMods(_copy);\n        // TODO handle creatureAdjustment for weak and elite variants\n        copyValues(type, copyFrom, target, _copy);\n        applyMods(originKey, copyFrom, target, _copy);\n        cleanupCopy(target, copyFrom);\n        return target;\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eMarkdown.java",
    "content": "package dev.ebullient.convert.tools.pf2e;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.io.MarkdownWriter;\nimport dev.ebullient.convert.io.MarkdownWriter.IndexContext;\nimport dev.ebullient.convert.qute.QuteNote;\nimport dev.ebullient.convert.tools.IndexType;\nimport dev.ebullient.convert.tools.MarkdownConverter;\nimport dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase;\nimport dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteNote;\n\npublic class Pf2eMarkdown implements MarkdownConverter {\n    final Pf2eIndex index;\n    final MarkdownWriter writer;\n\n    public Pf2eMarkdown(Pf2eIndex index, MarkdownWriter writer) {\n        this.index = index;\n        this.writer = writer;\n    }\n\n    @Override\n    public Pf2eMarkdown writeAll() {\n        return writeFiles(Stream.of(Pf2eIndexType.values())\n                .collect(Collectors.toList()));\n    }\n\n    @Override\n    public Pf2eMarkdown writeImages() {\n        index.tui().progressf(\"Writing images and fonts\");\n        index.tui().copyImages(Pf2eSources.getImages());\n        return this;\n    }\n\n    @Override\n    public Pf2eMarkdown writeFiles(IndexType type) {\n        return writeFiles(List.of(type));\n    }\n\n    static class WritingQueue {\n        List<Pf2eQuteBase> baseCompendium = new ArrayList<>();\n        List<Pf2eQuteBase> baseRules = new ArrayList<>();\n        List<QuteNote> noteCompendium = new ArrayList<>();\n        List<QuteNote> noteRules = new ArrayList<>();\n\n        // Some state for combining notes\n        Map<Pf2eIndexType, Json2QuteBase> combinedDocs = new HashMap<>();\n    }\n\n    @Override\n    public Pf2eMarkdown writeFiles(List<? extends IndexType> types) {\n        if (types == null || types.isEmpty()) {\n            return this;\n        }\n        index.tui().progressf(\"Converting data: %s\", types);\n        WritingQueue queue = new WritingQueue();\n        for (var entry : index.filteredEntries()) {\n            final String key = entry.getKey();\n            final JsonNode jsonSource = entry.getValue();\n\n            final Pf2eIndexType nodeType = Pf2eIndexType.getTypeFromKey(key);\n            if (!types.contains(nodeType)) {\n                continue;\n            }\n\n            if (nodeType.isOutputType() && !nodeType.useQuteNote()) {\n                writePf2eQuteBase(nodeType, key, jsonSource, queue);\n            } else if (nodeType.isOutputType() && nodeType.useQuteNote()) {\n                writeNotesAndTables(nodeType, key, jsonSource, queue);\n            }\n        }\n\n        IndexContext ctx = new IndexContext(MarkdownWriter::toTitle, (path) -> MarkdownWriter.sortEntryByTitle);\n\n        writer.writeFiles(index.compendiumFilePath(), queue.baseCompendium, ctx);\n        writer.writeFiles(index.rulesFilePath(), queue.baseRules, ctx);\n\n        for (Json2QuteBase value : queue.combinedDocs.values()) {\n            append(value.type, value.buildNote(), queue.noteCompendium, queue.noteRules);\n        }\n\n        // Custom indices\n        append(Pf2eIndexType.trait, Json2QuteTrait.buildIndex(index), queue.noteCompendium, queue.noteRules);\n\n        writer.writeNotes(index.compendiumFilePath(), queue.noteCompendium, true, ctx);\n        writer.writeNotes(index.rulesFilePath(), queue.noteRules, false, ctx);\n\n        writer.writeIndexes(ctx);\n\n        // TODO: DOES THIS WORK RIGHT? shouldn't these be in the other image map?\n        // List<ImageRef> images = rules.stream()\n        //         .flatMap(s -> s.images().stream()).collect(Collectors.toList());\n        // index.tui().copyImages(images, fallbackPaths);\n\n        return this;\n    }\n\n    private void writePf2eQuteBase(Pf2eIndexType type, String key, JsonNode node, WritingQueue queue) {\n        var compendium = queue.baseCompendium;\n        var rules = queue.baseRules;\n\n        // Moved to index type -- also used by embedded rendering\n        Pf2eQuteBase converted = type.convertJson2QuteBase(index, node);\n        if (converted != null) {\n            append(type, converted, compendium, rules);\n        }\n    }\n\n    private Pf2eMarkdown writeNotesAndTables(Pf2eIndexType type, String key, JsonNode node, WritingQueue queue) {\n        var compendium = queue.noteCompendium;\n        var rules = queue.noteRules;\n        var combinedDocs = queue.combinedDocs;\n\n        switch (type) {\n            case ability -> rules.add(new Json2QuteAbility(index, type, node).buildNote());\n            case affliction, curse, disease ->\n                compendium.add(new Json2QuteAffliction(index, type, node).buildNote());\n            case book -> {\n                index.tui().progressf(\"book %s\", key);\n                JsonNode data = index.getIncludedNode(key.replace(\"book|\", \"data|\"));\n                if (data == null) {\n                    index.tui().errorf(\"No data for %s\", key);\n                } else {\n                    List<Pf2eQuteNote> pages = new Json2QuteBook(index, type, node, data).buildBook();\n                    rules.addAll(pages);\n                }\n            }\n            case condition -> {\n                Json2QuteCompose conditions = (Json2QuteCompose) combinedDocs.computeIfAbsent(type,\n                        t -> new Json2QuteCompose(type, index, \"Conditions\"));\n                conditions.add(node);\n            }\n            case domain -> {\n                Json2QuteCompose domains = (Json2QuteCompose) combinedDocs.computeIfAbsent(type,\n                        t -> new Json2QuteCompose(type, index, \"Domains\"));\n                domains.add(node);\n            }\n            case skill -> {\n                Json2QuteCompose skills = (Json2QuteCompose) combinedDocs.computeIfAbsent(type,\n                        t -> new Json2QuteCompose(type, index, \"Skills\"));\n                skills.add(node);\n            }\n            case table -> {\n                Pf2eQuteNote table = new Json2QuteTable(index, node).buildNote();\n                rules.add(table);\n            }\n            default -> {\n            }\n        }\n\n        return this;\n    }\n\n    <T> void append(Pf2eIndexType type, T note, List<T> compendium, List<T> rules) {\n        if (note != null) {\n            if (type.useCompendiumBase()) {\n                compendium.add(note);\n            } else {\n                rules.add(note);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eSources.java",
    "content": "package dev.ebullient.convert.tools.pf2e;\n\nimport java.nio.file.Path;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.tools.CompendiumSources;\nimport dev.ebullient.convert.tools.IndexType;\nimport dev.ebullient.convert.tools.JsonTextConverter.SourceField;\nimport dev.ebullient.convert.tools.ToolsIndex.TtrpgValue;\nimport io.quarkus.qute.TemplateData;\n\n@TemplateData\npublic class Pf2eSources extends CompendiumSources {\n\n    private static final Map<String, Pf2eSources> keyToSources = new HashMap<>();\n    private static final Map<String, ImageRef> imageSourceToRef = new HashMap<>();\n\n    public static Pf2eSources findSources(String key) {\n        return keyToSources.get(key);\n    }\n\n    public static Pf2eSources findSources(JsonNode node) {\n        String key = TtrpgValue.indexKey.getTextOrEmpty(node);\n        return keyToSources.get(key);\n    }\n\n    public static Pf2eSources constructSources(Pf2eIndexType type, JsonNode node) {\n        if (node == null) {\n            throw new IllegalArgumentException(\"Must pass a JsonNode\");\n        }\n        String key = TtrpgValue.indexKey.getTextOrNull(node);\n        return keyToSources.computeIfAbsent(key, k -> {\n            Pf2eSources s = new Pf2eSources(type, key, node);\n            s.checkKnown();\n            return s;\n        });\n    }\n\n    public static Pf2eSources constructSyntheticSource(String name) {\n        String key = Pf2eIndexType.syntheticGroup.createKey(name, \"mixed\");\n        return new Pf2eSources(Pf2eIndexType.syntheticGroup, key, null);\n    }\n\n    public static Pf2eSources createEmbeddedSource(JsonNode node) {\n        if (node == null) {\n            throw new IllegalArgumentException(\"Must pass a JsonNode\");\n        }\n        String key = Pf2eIndexType.bookReference.createKey(node);\n        return new Pf2eSources(Pf2eIndexType.bookReference, key, node);\n    }\n\n    public static Pf2eSources findOrTemporary(Pf2eIndexType type, JsonNode node) {\n        if (node == null) {\n            throw new IllegalArgumentException(\"Must pass a JsonNode\");\n        }\n        String key = TtrpgValue.indexKey.getTextOrNull(node);\n        if (key == null) {\n            key = type.createKey(node);\n        }\n        Pf2eSources sources = findSources(key);\n        return sources == null\n                ? new Pf2eSources(type, key, node)\n                : sources;\n    }\n\n    public static ImageRef buildStreamImageRef(Pf2eIndex index, String sourcePath, Path relativeTarget, String title) {\n        ImageRef imageRef = new ImageRef.Builder()\n                .setStreamSource(sourcePath)\n                .setRelativePath(Path.of(\"assets\").resolve(relativeTarget))\n                .setTitle(index.replaceText(title))\n                .setRootFilepath(index.rulesFilePath())\n                .setVaultRoot(index.rulesVaultRoot())\n                .build(imageSourceToRef.get(sourcePath));\n        imageSourceToRef.putIfAbsent(sourcePath, imageRef);\n        return imageRef;\n    }\n\n    public static ImageRef buildImageRef(Pf2eIndexType type, Pf2eIndex index, Path sourcePath, String title) {\n        return buildImageRef(type, index, sourcePath, sourcePath, title);\n    }\n\n    public static ImageRef buildImageRef(Pf2eIndexType type, Pf2eIndex index, Path sourcePath, Path relativeTarget,\n            String title) {\n        String key = sourcePath.toString();\n        ImageRef imageRef = new ImageRef.Builder()\n                .setSourcePath(sourcePath)\n                .setRelativePath(Path.of(\"assets\").resolve(relativeTarget))\n                .setRootFilepath(type.getFilePath(index))\n                .setVaultRoot(type.getVaultRoot(index))\n                .setTitle(index.replaceText(title))\n                .build(imageSourceToRef.get(key));\n        imageSourceToRef.putIfAbsent(key, imageRef);\n        return imageRef;\n    }\n\n    public static Collection<ImageRef> getImages() {\n        return imageSourceToRef.values();\n    }\n\n    final Pf2eIndexType type;\n\n    private Pf2eSources(Pf2eIndexType type, String key, JsonNode node) {\n        super(type, key, node);\n        this.type = type;\n    }\n\n    public JsonNode findNode() {\n        return Pf2eIndex.findNode(this);\n    }\n\n    protected String findName(IndexType type, JsonNode node) {\n        if (type == Pf2eIndexType.syntheticGroup || type == Pf2eIndexType.bookReference) {\n            return this.key.replaceAll(\".*\\\\|(.*)\\\\|\", \"$1\");\n        }\n        String name = SourceField.name.getTextOrEmpty(node);\n        if (name.isEmpty()) {\n            throw new IllegalArgumentException(\"Unknown element, has no name: \" + node.toString());\n        }\n        return name;\n    }\n\n    @Override\n    protected String findSourceText(IndexType type, JsonNode jsonElement) {\n        if (type == Pf2eIndexType.syntheticGroup) {\n            return this.key.replaceAll(\".*\\\\|([^|]+)$\", \"$1\");\n        }\n        return super.findSourceText(type, jsonElement);\n    }\n\n    @Override\n    public Pf2eIndexType getType() {\n        return type;\n    }\n\n    /** Documents that have no primary source (compositions) */\n    protected boolean isSynthetic() {\n        return type == Pf2eIndexType.syntheticGroup;\n    }\n\n    public boolean fromDefaultSource() {\n        if (type == Pf2eIndexType.data) {\n            return true;\n        }\n        return type.defaultSourceString().equals(primarySource().toLowerCase());\n    }\n\n    public enum DefaultSource {\n        apg,\n        b1,\n        crb,\n        gmg,\n        locg,\n        lotg,\n        som;\n\n        public boolean sameSource(String source) {\n            return this.name().equalsIgnoreCase(source);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/Pf2eTypeReader.java",
    "content": "package dev.ebullient.convert.tools.pf2e;\n\npublic interface Pf2eTypeReader extends JsonSource {\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/qute/Pf2eQuteBase.java",
    "content": "package dev.ebullient.convert.tools.pf2e.qute;\n\nimport java.util.List;\n\nimport dev.ebullient.convert.config.TtrpgConfig;\nimport dev.ebullient.convert.qute.QuteBase;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.Pf2eIndexType;\nimport dev.ebullient.convert.tools.pf2e.Pf2eSources;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * Attributes for notes that are generated from the Pf2eTools data.\n * This is a trivial extension of {@link dev.ebullient.convert.qute.QuteBase QuteBase}.\n *\n * Notes created from {@code Pf2eQuteBase} will use a specific template\n * for the type. For example, {@code QuteBackground} will use {@code background2md.txt}.\n */\n@TemplateData\npublic class Pf2eQuteBase extends QuteBase {\n\n    protected final Pf2eIndexType type;\n\n    public Pf2eQuteBase(Pf2eSources sources, List<String> text, Tags tags) {\n        this(sources, sources.getName(), sources.getSourceText(), String.join(\"\\n\", text), tags);\n    }\n\n    public Pf2eQuteBase(Pf2eSources sources, String text, Tags tags) {\n        this(sources, sources.getName(), sources.getSourceText(), text, tags);\n    }\n\n    public Pf2eQuteBase(Pf2eSources sources, String name, String source, String text, Tags tags) {\n        super(sources, name, source, text, tags);\n        this.type = sources.getType();\n    }\n\n    public String title() {\n        return getName();\n    }\n\n    @Override\n    public String getVaultPath() {\n        String vaultRoot = type.useCompendiumBase()\n                ? TtrpgConfig.getConfig().compendiumVaultRoot()\n                : TtrpgConfig.getConfig().rulesVaultRoot();\n        String file = targetFile();\n        if (!file.endsWith(\".md\")) {\n            file += \".md\";\n        }\n        return vaultRoot + targetPath() + \"/\" + file;\n    }\n\n    @Override\n    public String targetPath() {\n        return type.relativePath();\n    }\n\n    @Override\n    public String targetFile() {\n        if (sources != null && !type.defaultSource().sameSource(sources.primarySource())) {\n            return getName() + \"-\" + sources.primarySource();\n        }\n        return getName();\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/qute/Pf2eQuteNote.java",
    "content": "package dev.ebullient.convert.tools.pf2e.qute;\n\nimport java.util.List;\n\nimport dev.ebullient.convert.qute.QuteNote;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.Pf2eIndexType;\nimport dev.ebullient.convert.tools.pf2e.Pf2eSources;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * Attributes for notes that are generated from the Pf2eTools data.\n * This is a trivial extension of {@link dev.ebullient.convert.qute.QuteNote QuteNote}.\n *\n * Notes created from {@code Pf2eQuteNote} will use the {@code note2md.txt} template\n * unless otherwise noted. Folder index notes use {@code index2md.txt}.\n */\n@TemplateData\npublic class Pf2eQuteNote extends QuteNote {\n    final Pf2eIndexType type;\n\n    public Pf2eQuteNote(Pf2eIndexType type, String name, String sourceText, List<String> text, Tags tags) {\n        this(type, name, sourceText, String.join(\"\\n\", text), tags);\n    }\n\n    public Pf2eQuteNote(Pf2eIndexType type, Pf2eSources sources, String name, List<String> text, Tags tags) {\n        super(sources, name, sources != null ? sources.getSourceText() : null, String.join(\"\\n\", text), tags);\n        this.type = type;\n    }\n\n    public Pf2eQuteNote(Pf2eIndexType type, Pf2eSources sources, String name, String text, Tags tags) {\n        super(sources, name, sources != null ? sources.getSourceText() : null, text, tags);\n        this.type = type;\n    }\n\n    public Pf2eQuteNote(Pf2eIndexType type, String name, String sourceText, String text, Tags tags) {\n        super(name, sourceText, text, tags);\n        this.type = type;\n    }\n\n    public Pf2eQuteNote(Pf2eIndexType type, Pf2eSources sources, String text, Tags tags) {\n        super(sources, sources.getName(), sources.getSourceText(), text, tags);\n        this.type = type;\n    }\n\n    public Pf2eQuteNote(Pf2eIndexType type, Pf2eSources sources, String name) { // custom indexes\n        super(sources, name, sources.getSourceText(), null, null);\n        this.type = type;\n    }\n\n    public Pf2eIndexType indexType() {\n        return type;\n    }\n\n    @Override\n    public String targetPath() {\n        return type.relativePath();\n    }\n\n    @Override\n    public String targetFile() {\n        if (sources != null && !type.defaultSource().sameSource(sources.primarySource())) {\n            return getName() + \"-\" + sources.primarySource();\n        }\n        return getName();\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAbility.java",
    "content": "package dev.ebullient.convert.tools.pf2e.qute;\n\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\nimport dev.ebullient.convert.qute.QuteUtil;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.JsonSource;\nimport dev.ebullient.convert.tools.pf2e.Pf2eIndexType;\nimport dev.ebullient.convert.tools.pf2e.Pf2eSources;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * Pf2eTools Ability attributes ({@code ability2md.txt} or {@code inline-ability2md.txt}).\n *\n * Abilities are rendered both standalone and inline (as an admonition block).\n * The default template can render both. It contains some special syntax to handle\n * the inline case.\n *\n * Use `%%--` to mark the end of the preamble (frontmatter and\n * other leading content only appropriate to the standalone case).\n *\n * Extension of {@link dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteNote Pf2eQuteNote}\n */\n@TemplateData\npublic final class QuteAbility extends Pf2eQuteNote implements QuteUtil.Renderable, QuteAbilityOrAffliction {\n\n    /** A formatted string which is a link to the base ability that this ability references. Embedded only. */\n    public final String reference;\n    /**\n     * Collection of trait links. Use `{#for}` or `{#each}` to iterate over the collection.\n     * See [traitList](#traitlist) or [bareTraitList](#baretraitlist).\n     */\n    public final Collection<String> traits;\n    /** {@link QuteDataRange}. The targeting range for this ability. */\n    public final QuteDataRange range;\n    /** List of formatted strings. Activation components for this ability, e.g. command, envision */\n    public final List<String> components;\n    /** Formatted string. Trigger to activate this ability */\n    public final String trigger;\n    /** Formatted string. Requirements for activating this ability */\n    public final String requirements;\n    /** Formatted string. Prerequisites before this ability can be activated or taken. */\n    public final String prerequisites;\n    /**\n     * {@link dev.ebullient.convert.tools.pf2e.qute.QuteDataFrequency QuteDataFrequency}.\n     * How often this ability can be used/activated. Use directly to get a formatted string.\n     */\n    public final QuteDataFrequency frequency;\n    /** The cost of using this ability */\n    public final String cost;\n    /** Any additional notes related to this ability that aren't included in the other fields. */\n    public final String note;\n    /** Special notes for this ability - usually requirements or caveats relating to its use. */\n    public final String special;\n    /**\n     * True if this ability is embedded in another note (admonition block).\n     * When this is true, the {@code inline-ability} template is used.\n     */\n    public final boolean embedded;\n    /** Ability ({@link dev.ebullient.convert.tools.pf2e.qute.QuteDataActivity activity/activation details}) */\n    public final QuteDataActivity activity;\n\n    // Internal only.\n    private final JsonSource _converter;\n\n    public QuteAbility(Pf2eSources sources, String name, String reference, String text, Tags tags,\n            Collection<String> traits, QuteDataActivity activity, QuteDataRange range,\n            List<String> components, String requirements, String prerequisites,\n            String cost, String trigger, QuteDataFrequency frequency, String special, String note,\n            boolean embedded, JsonSource converter) {\n        super(Pf2eIndexType.ability, sources, name, text, tags);\n\n        this.reference = reference;\n        this.traits = traits;\n        this.activity = activity;\n        this.range = range;\n        this.components = components;\n        this.requirements = requirements;\n        this.prerequisites = prerequisites;\n        this.cost = cost;\n        this.trigger = trigger;\n        this.frequency = frequency;\n        this.special = special;\n        this.note = note;\n        this.embedded = embedded;\n        this._converter = converter;\n    }\n\n    /** True if an activity (with text), components, or traits are present. */\n    public boolean getHasActivity() {\n        return activity != null || isPresent(components) || isPresent(traits);\n    }\n\n    /**\n     * True if hasActivity is true, hasEffect is true or cost is present.\n     * In other words, this is true if a list of attributes could have been rendered.\n     *\n     * Use this to test for the end of those attributes (add whitespace or a special\n     * character ahead of ability text)\n     */\n    public boolean getHasAttributes() {\n        return getHasActivity() || getHasEffect() || isPresent(cost);\n    }\n\n    /**\n     * True if the ability is a short, one-line name and description.\n     * Use this to test to choose between a detailed or simple rendering.\n     */\n    public boolean getHasDetails() {\n        return getHasAttributes() || isPresent(special) || text.contains(\"\\n\") || text.split(\" \").length > 5;\n    }\n\n    @Deprecated\n    public boolean getHasBullets() {\n        return getHasAttributes();\n    }\n\n    /** True if frequency, trigger, and requirements are present. In other words, this is true if the ability has an effect. */\n    public boolean getHasEffect() {\n        return isPresent(frequency) || isPresent(trigger) || isPresent(requirements);\n    }\n\n    /** Return a comma-separated list of de-styled trait links (no title attributes) */\n    public String getBareTraitList() {\n        if (traits == null || traits.isEmpty()) {\n            return \"\";\n        }\n        return traits.stream()\n                .map(x -> x.replaceAll(\" \\\".*\\\"\", \"\"))\n                .collect(Collectors.joining(\", \"));\n    }\n\n    @Override\n    public String template() {\n        return embedded ? \"inline-ability2md.txt\" : \"ability2md.txt\";\n    }\n\n    @Override\n    public String toString() {\n        return render();\n    }\n\n    @Override\n    public String render() {\n        return _converter.renderEmbeddedTemplate(this, null);\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAbilityOrAffliction.java",
    "content": "package dev.ebullient.convert.tools.pf2e.qute;\n\nimport dev.ebullient.convert.qute.QuteUtil;\nimport dev.ebullient.convert.tools.pf2e.Pf2eIndexType;\n\n/**\n * A union type which is either a {@link dev.ebullient.convert.tools.pf2e.qute.QuteAbility QuteAbility}\n * or a {@link dev.ebullient.convert.tools.pf2e.qute.QuteAffliction QuteAffliction}.\n *\n * Use {@link dev.ebullient.convert.tools.pf2e.qute.QuteAbilityOrAffliction#isAbility() isAbility()}\n * and {@link dev.ebullient.convert.tools.pf2e.qute.QuteAbilityOrAffliction#isAffliction() isAffliction()}\n * to tell whether it's an ability or an affliction.\n */\npublic sealed interface QuteAbilityOrAffliction extends QuteUtil permits QuteAbility, QuteAffliction {\n    /** Returns true if this object is a {@link dev.ebullient.convert.tools.pf2e.qute.QuteAbility QuteAbility} */\n    default boolean isAbility() {\n        return indexType() == Pf2eIndexType.ability;\n    }\n\n    /** Returns true if this object is a {@link dev.ebullient.convert.tools.pf2e.qute.QuteAffliction QuteAffliction} */\n    default boolean isAffliction() {\n        return switch ((Pf2eIndexType) indexType()) {\n            case affliction, disease, curse -> true;\n            default -> false;\n        };\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAction.java",
    "content": "package dev.ebullient.convert.tools.pf2e.qute;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\n\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.Pf2eSources;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * Pf2eTools Action attributes ({@code action2md.txt})\n *\n * Extension of {@link dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase Pf2eQuteBase}\n */\n@TemplateData\npublic class QuteAction extends Pf2eQuteBase {\n\n    /** Trigger for this action */\n    public final String trigger;\n    /** Collection of traits (decorated links) */\n    public final Collection<String> traits;\n    /** Situational requirements for performing this action */\n    public final String requirements;\n    /** Prerequisite trait or characteristic for performing this action */\n    public final String prerequisites;\n    /**\n     * {@link dev.ebullient.convert.tools.pf2e.qute.QuteDataFrequency QuteDataFrequency}.\n     * How often this action can be used/activated. Use directly to get a formatted string.\n     */\n    public final QuteDataFrequency frequency;\n    /** The cost of using this action */\n    public final String cost;\n    /** Type of action (as {@link dev.ebullient.convert.tools.pf2e.qute.QuteAction.ActionType ActionType}) */\n    public final ActionType actionType;\n    /** Activity/Activation cost (as {@link dev.ebullient.convert.tools.pf2e.qute.QuteDataActivity QuteDataActivity}) */\n    public final QuteDataActivity activity;\n\n    private final List<String> altNames;\n\n    public QuteAction(Pf2eSources sources, List<String> text, Tags tags,\n            String cost, String trigger, List<String> aliases, Collection<String> traits,\n            String prerequisites, String requirements, QuteDataFrequency frequency,\n            QuteDataActivity activity, ActionType actionType) {\n        super(sources, text, tags);\n        this.trigger = trigger;\n        this.altNames = aliases;\n        this.traits = traits;\n\n        this.prerequisites = prerequisites;\n        this.requirements = requirements;\n        this.cost = cost;\n        this.frequency = frequency;\n\n        this.activity = activity;\n        this.actionType = actionType;\n    }\n\n    @Override\n    public List<String> getAltNames() {\n        // Used by getAliases in QuteBase\n        return altNames;\n    }\n\n    /** True if this is a basic action. Same as `{resource.actionType.basic}`. */\n    public boolean isBasic() {\n        return actionType != null && actionType.basic;\n    }\n\n    /** True if this action is an item action. Same as `{resource.actionType.item}`. */\n    public boolean isItem() {\n        return actionType != null && actionType.item;\n    }\n\n    /**\n     * Pf2eTools Action type attributes.\n     *\n     *\n     * This data object provides a default mechanism for creating\n     * a marked up string based on the attributes that are present.\n     * To use it, reference this attribute directly: `{resource.actionType}`.\n     *\n     */\n    @TemplateData\n    public static class ActionType {\n        /** True if this is a basic action */\n        public final boolean basic;\n        /** True if this an item action */\n        public final boolean item;\n        /** Skills used or required by this action */\n        public final String skills;\n        /** List of ancestries associated with this action */\n        public final List<String> ancestry;\n        /** List of archetypes associated with this action */\n        public final List<String> archetype;\n        /** List of heritages associated with this action */\n        public final List<String> heritage;\n        /** List of versatile heritages associated with this action */\n        public final List<String> versatileHeritage;\n        /** List of classes associated with this action */\n        public final List<String> classType;\n        /** List of subclasses associated with this action */\n        public final List<String> subclass;\n        /** List of variant rules associated with this action */\n        public final List<String> variantrule;\n\n        public ActionType(\n                boolean basic, boolean item, String skills,\n                List<String> classType, List<String> subclass, List<String> archetype,\n                List<String> ancestry, List<String> heritage, List<String> versatileHeritage,\n                List<String> variantrule) {\n            this.basic = basic;\n            this.item = item;\n            this.skills = skills;\n            this.archetype = archetype;\n            this.ancestry = ancestry;\n            this.heritage = heritage;\n            this.versatileHeritage = versatileHeritage;\n            this.classType = classType;\n            this.subclass = subclass;\n            this.variantrule = variantrule;\n        }\n\n        public String toString() {\n            List<String> entries = new ArrayList<>();\n\n            // skill (trained, untrained, expert, legendary)\n            if (skills != null && !skills.isEmpty()) {\n                entries.add(\"**Skill** \" + skills);\n            }\n\n            if (classType != null && !classType.isEmpty()) {\n                List<String> inner = new ArrayList<>();\n                inner.add(\"**Class** \" + String.join(\", \", classType));\n                if (subclass != null && !subclass.isEmpty()) {\n                    inner.add(\"**Subclass** \" + String.join(\", \", subclass));\n                }\n                entries.add(String.join(\"; \", inner));\n            }\n\n            if (archetype != null && !archetype.isEmpty()) {\n                entries.add(\"**Archetype** \" + String.join(\", \", archetype));\n            }\n\n            if (ancestry != null && !ancestry.isEmpty()) {\n                List<String> inner = new ArrayList<>();\n                inner.add(\"**Ancestry** \" + String.join(\", \", ancestry));\n                if (heritage != null && !heritage.isEmpty()) {\n                    inner.add(\"**Heritage** \" + String.join(\", \", heritage));\n                }\n                if (versatileHeritage != null && !versatileHeritage.isEmpty()) {\n                    inner.add(\"**Versatile Heritage** \" + String.join(\", \", versatileHeritage));\n                }\n                entries.add(String.join(\"; \", inner));\n            }\n\n            if (variantrule != null && !variantrule.isEmpty()) {\n                entries.add(\"**Variant Rule** \" + String.join(\", \", variantrule));\n            }\n            return String.join(\"\\n\", entries);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteAffliction.java",
    "content": "package dev.ebullient.convert.tools.pf2e.qute;\n\nimport static dev.ebullient.convert.StringUtil.join;\nimport static dev.ebullient.convert.StringUtil.toTitleCase;\n\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\n\nimport dev.ebullient.convert.qute.QuteUtil;\nimport dev.ebullient.convert.tools.JsonTextConverter;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.Pf2eIndexType;\nimport dev.ebullient.convert.tools.pf2e.Pf2eSources;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * Pf2eTools Affliction attributes (inline/embedded, {@code inline-affliction2md.txt})\n *\n * Extension of {@link dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteNote Pf2eQuteNote}\n */\n@TemplateData\npublic final class QuteAffliction extends Pf2eQuteNote implements QuteUtil.Renderable, QuteAbilityOrAffliction {\n\n    /** Collection of traits (decorated links) */\n    public final Collection<String> traits;\n    /** Integer from 1 to 10. Level of the affliction. */\n    public final String level;\n\n    /** Category of affliction (Curse or Disease). Usually shown alongside the level. */\n    public final String category;\n    /** A description of the tempted version of the curse */\n    public final String temptedCurse;\n    /** Formatted text. Maximum duration of the infliction. */\n    public final String maxDuration;\n    /** Formatted text. Maximum duration of the infliction. */\n    public final String onset;\n    /**\n     * The saving throw required to not contract or advance the affliction as\n     * {@link dev.ebullient.convert.tools.pf2e.qute.QuteAffliction.QuteAfflictionSave QuteAfflictionSave}\n     */\n    public final QuteAfflictionSave savingThrow;\n    /** Formatted text. Affliction effect, may be multiple lines. */\n    public final String effect;\n    /**\n     * Affliction stages: map of name to stage data as\n     * {@link dev.ebullient.convert.tools.pf2e.qute.QuteAffliction.QuteAfflictionStage QuteAfflictionStage}\n     */\n    public final Map<String, QuteAfflictionStage> stages;\n    /** If true, then this affliction is embedded into a larger note. */\n    public final boolean isEmbedded;\n    /** Any additional notes associated with the affliction. */\n    public final List<String> notes;\n\n    // Internal only\n    private final JsonTextConverter<?> _converter;\n    private final List<String> altNames;\n\n    public QuteAffliction(\n            Pf2eSources sources, String name, List<String> text, Tags tags,\n            Collection<String> traits, List<String> aliases, String level,\n            String category, String maxDuration, String onset, QuteAfflictionSave savingThrow,\n            String effect, String temptedCurse, List<String> notes, Map<String, QuteAfflictionStage> stages,\n            boolean isEmbedded, JsonTextConverter<?> _converter) {\n        super(Pf2eIndexType.affliction, sources, name, text, tags);\n\n        this.level = level;\n        this.traits = traits;\n        this.maxDuration = maxDuration;\n        this.onset = onset;\n        this.savingThrow = savingThrow;\n        this.effect = effect;\n        this.stages = stages;\n        this.temptedCurse = temptedCurse;\n        this.altNames = aliases;\n        this.category = category;\n        this.isEmbedded = isEmbedded;\n        this.notes = notes;\n        this._converter = _converter;\n    }\n\n    @Override\n    public List<String> getAltNames() {\n        // Used by getAliases in QuteBase\n        return altNames;\n    }\n\n    /** The category and level as a formatted string, e.g. \"Disease 9\" */\n    public String formattedLevel() {\n        return join(\" \",\n                // Use \"Level\" as the default category, but only if we don't already have \"Level\" in the level text\n                category == null && level != null && !level.toLowerCase().startsWith(\"level \")\n                        ? \"Level\"\n                        : toTitleCase(category),\n                level);\n    }\n\n    @Override\n    public String template() {\n        return isEmbedded ? \"inline-affliction2md.txt\" : \"affliction2md.txt\";\n    }\n\n    @Override\n    public String render() {\n        return _converter.renderInlineTemplate(this, null);\n    }\n\n    @Override\n    public String toString() {\n        return render();\n    }\n\n    /**\n     * True if the affliction specifies a tempting curse or has stages. If you use a section header\n     * for curse information, use this test to add a section header before other text.\n     */\n    @Override\n    public boolean getHasSections() {\n        return isPresent(temptedCurse) || !stages.isEmpty();\n    }\n\n    /**\n     * Pf2eTools affliction stage attributes.\n     *\n     * @param text Formatted text. Affliction stage\n     * @param duration Formatted text. Affliction duration\n     */\n    @TemplateData\n    public record QuteAfflictionStage(String duration, String text) {\n    }\n\n    /**\n     * Affliction saving throw\n     *\n     * @param value The DC of the saving throw\n     * @param notes Any notes relating to the saving throw\n     * @param save The type of save associated with the throw e.g. Fortitude\n     */\n    @TemplateData\n    public record QuteAfflictionSave(Integer value, String save, List<String> notes) implements QuteDataGenericStat {\n        public QuteAfflictionSave(Integer value, String save, String note) {\n            this(value, save, note != null && !note.isBlank() ? List.of(note) : List.of());\n        }\n\n        @Override\n        public String formattedNotes() {\n            return String.join(\", \", notes);\n        }\n\n        @Override\n        public String toString() {\n            return join(\" \",\n                    save, isPresent(value) || !notes.isEmpty() ? \"DC\" : \"\", value, formattedNotes());\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteArchetype.java",
    "content": "package dev.ebullient.convert.tools.pf2e.qute;\n\nimport java.util.Collection;\nimport java.util.List;\n\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.Pf2eSources;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * Pf2eTools Archetype attributes ({@code archetype2md.txt})\n *\n * Extension of {@link dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase Pf2eQuteBase}\n */\n@TemplateData\npublic class QuteArchetype extends Pf2eQuteBase {\n\n    /** Collection of traits (decorated links) */\n    public final Collection<String> traits;\n\n    public final int dedicationLevel;\n    public final List<String> benefits;\n    public final List<String> feats;\n\n    public QuteArchetype(Pf2eSources sources, List<String> text, Tags tags,\n            Collection<String> traits, int dedicationLevel, List<String> benefits, List<String> feats) {\n        super(sources, text, tags);\n\n        this.traits = traits;\n        this.dedicationLevel = dedicationLevel;\n        this.benefits = benefits;\n        this.feats = feats;\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteBackground.java",
    "content": "package dev.ebullient.convert.tools.pf2e.qute;\n\nimport java.util.List;\n\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.Pf2eSources;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * Pf2eTools Background attributes ({@code background2md.txt})\n *\n * Extension of {@link dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase Pf2eQuteBase}\n */\n@TemplateData\npublic class QuteBackground extends Pf2eQuteBase {\n\n    public QuteBackground(Pf2eSources sources, List<String> text, Tags tags) {\n        super(sources, text, tags);\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteBook.java",
    "content": "package dev.ebullient.convert.tools.pf2e.qute;\n\nimport java.util.List;\n\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.Pf2eIndexType;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * Pf2eTools Book attributes ({@code book2md.txt})\n *\n * Extension of {@link dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteNote Pf2eQuteNote}\n */\n@TemplateData\npublic class QuteBook extends Pf2eQuteNote {\n\n    public final List<String> altNames;\n    /** Information about the book as {@code dev.ebullient.convert.tools.pf2e.qute.QuteBook.BookInfo} */\n    public final BookInfo bookInfo;\n\n    final String bookDir;\n\n    public QuteBook(String name, List<String> text, Tags tags, String bookDir, BookInfo bookInfo,\n            List<String> aliases) {\n        super(Pf2eIndexType.book, name, null, text, tags);\n        this.bookDir = bookDir;\n        this.bookInfo = bookInfo;\n        this.altNames = aliases;\n    }\n\n    @Override\n    public List<String> getAltNames() {\n        // Used by getAliases in QuteBase/Pf2eQuteNote\n        return altNames;\n    }\n\n    @Override\n    public String targetPath() {\n        return bookDir;\n    }\n\n    @Override\n    public String targetFile() {\n        return this.getName();\n    }\n\n    @Override\n    public String template() {\n        return \"book2md.txt\";\n    }\n\n    /**\n     * Pf2eTools book information\n     *\n     *\n     * This data object provides a default mechanism for creating\n     * a marked up string based on the attributes that are present.\n     * To use it, reference it directly: `{resource.actionType}`.\n     *\n     */\n    @TemplateData\n    public static class BookInfo {\n        /** Name of the book */\n        public String name;\n        /** Book id */\n        public String id;\n        /** Date published */\n        public String published;\n        /** Group this book belongs to (core, lost-omens, supplement, etc.) */\n        public String group;\n        /** Author */\n        public String author;\n        /** Cover image as {@code dev.ebullient.convert.qute.ImageRef} */\n        public ImageRef cover;\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteCreature.java",
    "content": "package dev.ebullient.convert.tools.pf2e.qute;\n\nimport static dev.ebullient.convert.StringUtil.flatJoin;\nimport static dev.ebullient.convert.StringUtil.format;\nimport static dev.ebullient.convert.StringUtil.join;\nimport static dev.ebullient.convert.StringUtil.parenthesize;\nimport static dev.ebullient.convert.StringUtil.pluralize;\nimport static dev.ebullient.convert.StringUtil.toOrdinal;\nimport static dev.ebullient.convert.StringUtil.toTitleCase;\n\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\nimport dev.ebullient.convert.StringUtil;\nimport dev.ebullient.convert.io.JavadocVerbatim;\nimport dev.ebullient.convert.qute.QuteUtil;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.Pf2eSources;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * Pf2eTools Creature attributes ({@code creature2md.txt})\n *\n * Extension of {@link Pf2eQuteBase Pf2eQuteBase}\n */\n@TemplateData\npublic class QuteCreature extends Pf2eQuteBase {\n\n    public final List<String> altNames;\n    /** Collection of traits (decorated links, optional) */\n    public final Collection<String> traits;\n    /** Short creature description (optional) */\n    public final String description;\n    /** Creature level (number, optional) */\n    public final Integer level;\n    /** Creature perception (number, optional) */\n    public final Integer perception;\n    /**\n     * Languages as {@link QuteCreature.CreatureLanguages CreatureLanguages}\n     */\n    public final CreatureLanguages languages;\n    /** Defenses (AC, saves, etc) as {@link QuteDataDefenses QuteDataDefenses} */\n    public final QuteDataDefenses defenses;\n    /**\n     * Skill bonuses as {@link QuteCreature.CreatureSkills CreatureSkills}\n     */\n    public final CreatureSkills skills;\n    /** Senses as a list of {@link QuteCreature.CreatureSense CreatureSense} */\n    public final List<CreatureSense> senses;\n    /** Ability modifiers as a map of (name, modifier) */\n    public final Map<String, Integer> abilityMods;\n    /** Items held by the creature as a list of strings */\n    public final List<String> items;\n    /** The creature's speed, as an {@link QuteDataSpeed QuteDataSpeed} */\n    public final QuteDataSpeed speed;\n    /** The creature's attacks, as a list of {@link QuteInlineAttack QuteInlineAttack} */\n    public final List<QuteInlineAttack> attacks;\n\n    /**\n     * The creature's abilities, as a\n     * {@link QuteCreature.CreatureAbilities CreatureAbilities}.\n     */\n    public final CreatureAbilities abilities;\n    /** The creature's spellcasting capabilities, as a list of {@link QuteCreature.CreatureSpellcasting} */\n    public final List<CreatureSpellcasting> spellcasting;\n    /** The creature's ritual casting capabilities, as a list of {@link QuteCreature.CreatureRitualCasting} */\n    public final List<CreatureRitualCasting> ritualCasting;\n\n    public QuteCreature(\n            Pf2eSources sources, String text, Tags tags,\n            Collection<String> traits, List<String> aliases,\n            String description, Integer level, Integer perception,\n            QuteDataDefenses defenses, CreatureLanguages languages, CreatureSkills skills,\n            List<CreatureSense> senses, Map<String, Integer> abilityMods,\n            List<String> items, QuteDataSpeed speed,\n            List<QuteInlineAttack> attacks, CreatureAbilities abilities,\n            List<CreatureSpellcasting> spellcasting, List<CreatureRitualCasting> ritualCasting) {\n        super(sources, text, tags);\n        this.traits = traits;\n        this.altNames = aliases;\n        this.description = description;\n        this.level = level;\n        this.perception = perception;\n        this.languages = languages;\n        this.defenses = defenses;\n        this.skills = skills;\n        this.senses = senses;\n        this.abilityMods = abilityMods;\n        this.items = items;\n        this.speed = speed;\n        this.attacks = attacks;\n        this.abilities = abilities;\n        this.spellcasting = spellcasting;\n        this.ritualCasting = ritualCasting;\n    }\n\n    @Override\n    public List<String> getAltNames() {\n        // Used by getAliases in QuteBase/Pf2eQuteNote\n        return altNames;\n    }\n\n    /**\n     * The languages and language features known by a creature. Example default output:\n     * `Common, Sylvan; telepathy 100ft; knows any language the summoner does`\n     *\n     * @param languages Languages known (optional)\n     * @param notes Language-related notes (optional)\n     * @param abilities Language-related abilities (optional)\n     */\n    @TemplateData\n    public record CreatureLanguages(\n            List<String> languages,\n            List<String> notes,\n            List<String> abilities) implements QuteUtil {\n\n        @Override\n        public String toString() {\n            return flatJoin(\"; \", List.of(join(\", \", languages)), abilities, notes);\n        }\n    }\n\n    /**\n     * A creature's skill information. Example default output:\n     *\n     * ```md\n     * Athletics +10, Cult Lore +10 (lore on their cult), Stealth +10 (+12 in forests); Some skill note\n     * ```\n     *\n     * @param skills Skill bonuses for the creature, as a list of\n     *        {@link QuteDataGenericStat.QuteDataNamedBonus QuteDataNamedBonus}\n     * @param notes Notes for the creature's skills (list of strings, optional)\n     */\n    @TemplateData\n    public record CreatureSkills(\n            List<QuteDataGenericStat.QuteDataNamedBonus> skills,\n            List<String> notes) {\n\n        @Override\n        public String toString() {\n            return flatJoin(\"; \", List.of(join(\", \", skills)), notes);\n        }\n    }\n\n    /**\n     * A creature's senses. Example default output: `tremorsense (imprecise) 20ft`\n     *\n     * @param name The name of the sense (required, string)\n     * @param type The type of the sense - e.g. precise, imprecise (optional, string)\n     * @param range The range of the sense (optional, integer)\n     */\n    @TemplateData\n    public record CreatureSense(String name, String type, Integer range) implements QuteUtil {\n\n        @Override\n        public String toString() {\n            return join(\" \", name, parenthesize(type), range);\n        }\n    }\n\n    public enum SpellcastingTradition {\n        arcane,\n        divine,\n        occult,\n        primal;\n    }\n\n    public enum SpellcastingPreparation {\n        innate,\n        prepared,\n        spontaneous,\n        focus;\n    }\n\n    /**\n     * A creature's abilities, split into the section of the statblock where they should be displayed. Each section is\n     * a list of {@link QuteAbilityOrAffliction}. Using an entry in one of these lists directly\n     * will give you a pre-formatted ability according to the embedded template defined for {@link QuteAbility} or\n     * {@link QuteAffliction} as appropriate.\n     *\n     * @param top Abilities which should be displayed in the top section of the statblock\n     * @param middle Abilities which should be displayed in the middle section of the statblock\n     * @param bottom Abilities which should be displayed in the bottom section of the statblock\n     */\n    @TemplateData\n    public record CreatureAbilities(\n            List<QuteAbilityOrAffliction> top,\n            List<QuteAbilityOrAffliction> middle,\n            List<QuteAbilityOrAffliction> bottom) implements QuteUtil {\n    }\n\n    /**\n     * Information about a type of ritual casting available to this creature.\n     *\n     * @param tradition The tradition for these rituals\n     * @param dc The spell save DC for these rituals\n     * @param ranks The ritual ranks, as a list of {@link QuteCreature.CreatureSpells}\n     */\n    @TemplateData\n    public record CreatureRitualCasting(\n            SpellcastingTradition tradition,\n            Integer dc,\n            List<CreatureSpells> ranks) {\n        /** The name of this set of rituals, e.g. \"Divine Rituals\" */\n        public String name() {\n            return join(\" \", tradition, \"Rituals\");\n        }\n    }\n\n    /**\n     * Information about a type of spellcasting available to this creature.\n     *\n     * @param customName A custom name for this set of spells, e.g. \"Champion Devotion Spells\". Use\n     *        {@link QuteCreature.CreatureSpellcasting#name()} to get a name which takes this into account\n     *        if it exists.\n     * @param preparation The type of preparation for these spells, as a {@link QuteCreature.SpellcastingPreparation}\n     * @param tradition The tradition for these spells, as a {@link QuteCreature.SpellcastingTradition}\n     * @param focusPoints The number of focus points available to this creature for these spells. Present only if these\n     *        are focus spells.\n     * @param attackBonus The spell attack bonus for these spells (integer)\n     * @param dc The spell save DC for these spells (integer)\n     * @param notes Any notes associated with these spells\n     * @param ranks The spells for each rank, as a list of {@link QuteCreature.CreatureSpells}.\n     * @param constantRanks The constant spells for each rank, as a list of {@link QuteCreature.CreatureSpells}\n     */\n    @TemplateData\n    public record CreatureSpellcasting(\n            String customName,\n            SpellcastingPreparation preparation,\n            SpellcastingTradition tradition,\n            Integer focusPoints,\n            Integer attackBonus,\n            Integer dc,\n            List<String> notes,\n            List<CreatureSpells> ranks,\n            List<CreatureSpells> constantRanks) {\n        /**\n         * The name for this set of spells. This is either the custom name, or derived from the tradition and\n         * preparation - e.g. \"Occult Prepared Spells\", or \"Divine Innate Spells\".\n         */\n        @JavadocVerbatim\n        public String name() {\n            return customName != null && !customName.isBlank()\n                    ? customName\n                    : toTitleCase(join(\" \", tradition, preparation, \"Spells\"));\n        }\n\n        /**\n         * Stats for this kind of spellcasting, including the DC, attack bonus, and any focus points.\n         *\n         * ```md\n         * DC 20, attack +25, 2 Focus Points\n         * ```\n         */\n        @JavadocVerbatim\n        public String formattedStats() {\n            return join(\", \",\n                    format(\"DC %d\", dc),\n                    format(\"attack %+d\", attackBonus),\n                    focusPoints == null ? \"\" : focusPoints + \" Focus \" + pluralize(\"Point\", focusPoints));\n        }\n    }\n\n    /**\n     * A collection of spells with some additional information.\n     *\n     * ```md\n     * **Cantrips (9th)** [daze](#), [shadow siphon](#) (acid only) (×2)\n     * ```\n     *\n     * ```md\n     * **4th** [confusion](#), [phantasmal killer](#) (2 slots)\n     * ```\n     *\n     * @param knownRank The rank that these spells are known at (0 for cantrips). May be absent for rituals.\n     * @param cantripRank The rank that these spells are auto-heightened to. Present only for cantrips.\n     * @param slots The number of slots available for these spells. Not present for constant spells or rituals.\n     * @param spells A list of spells, as a list of {@link QuteCreature.CreatureSpellReference}\n     */\n    @TemplateData\n    public record CreatureSpells(\n            Integer knownRank,\n            Integer cantripRank,\n            Integer slots,\n            List<CreatureSpellReference> spells) {\n\n        public CreatureSpells(Integer rank, List<CreatureSpellReference> spells) {\n            this(rank, null, null, spells);\n        }\n\n        /** True if these are cantrip spells */\n        public boolean isCantrips() {\n            return knownRank != null && knownRank == 0;\n        }\n\n        /** The rank for this set of spells, with appropriate cantrip handling. e.g. \"5th\", or \"Cantrips (9th)\" */\n        public String rank() {\n            if (knownRank == null) {\n                return \"\";\n            }\n            return isCantrips() ? \"Cantrips \" + parenthesize(toOrdinal(cantripRank)) : toOrdinal(knownRank);\n        }\n\n        @Override\n        public String toString() {\n            return join(\" \",\n                    format(\"**%s**\", rank()),\n                    join(\", \", spells),\n                    format(\"(%d slots)\", slots));\n        }\n    }\n\n    /**\n     * A spell known by the creature.\n     *\n     * ```md\n     * [shadow siphon](#) (acid only) (×2)\n     * ```\n     *\n     * @param name The name of the spell\n     * @param link A formatted link to the spell's note, or just the spell's name if we couldn't get a link.\n     * @param amount The number of casts available for this spell. A value of 0 represents an at will spell. Use\n     *        {@link QuteCreature.CreatureSpellReference#formattedAmount()} to get this as a formatted string.\n     * @param notes Any notes associated with this spell, e.g. \"at will only\"\n     */\n    @TemplateData\n    public record CreatureSpellReference(\n            String name,\n            String link,\n            Integer amount,\n            List<String> notes) {\n\n        /** The number of casts as a formatted string, e.g. \"(at will)\" or \"(×2)\". Empty when the amount is 1. */\n        public String formattedAmount() {\n            return amount == 1 ? \"\" : parenthesize(amount == 0 ? \"at will\" : \"×\" + amount);\n        }\n\n        public String formattedNotes() {\n            return notes.stream().map(StringUtil::parenthesize).collect(Collectors.joining(\" \"));\n        }\n\n        @Override\n        public String toString() {\n            return join(\" \", link, formattedAmount(), formattedNotes());\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataActivity.java",
    "content": "package dev.ebullient.convert.tools.pf2e.qute;\n\nimport static dev.ebullient.convert.StringUtil.join;\n\nimport dev.ebullient.convert.qute.ImageRef;\nimport dev.ebullient.convert.qute.QuteUtil;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * Pf2eTools activity attributes. This attribute will render itself as a formatted link:\n *\n * <pre>\n *     [textGlyph](rulesPath \"glyph.title\")&lt;optional text&gt;\n * </pre>\n *\n * @param text The text associated with the action - may be null.\n * @param glyph icon/image representing this activity as a {@link dev.ebullient.convert.qute.ImageRef ImageRef}\n * @param textGlyph A textual representation of the glyph, used as the link text\n * @param rulesPath The path which leads to an explanation of this particular activity\n */\n@TemplateData\npublic record QuteDataActivity(String text, ImageRef glyph, String textGlyph,\n        String rulesPath) implements QuteUtil, QuteDataDuration {\n\n    /** Return the text associated with the action. */\n    @Override\n    public String text() {\n        return isPresent(text) ? text : glyph.title;\n    }\n\n    public String toString() {\n        return join(\" \", \"[%s](%s \\\"%s\\\")\".formatted(textGlyph, rulesPath, glyph.title), text);\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataArmorClass.java",
    "content": "package dev.ebullient.convert.tools.pf2e.qute;\n\nimport static dev.ebullient.convert.StringUtil.flatJoin;\nimport static dev.ebullient.convert.StringUtil.formatMap;\nimport static dev.ebullient.convert.StringUtil.join;\nimport static dev.ebullient.convert.StringUtil.parenthesize;\n\nimport java.util.List;\nimport java.util.Map;\n\nimport io.quarkus.qute.TemplateData;\n\n/**\n * Pf2eTools armor class attributes.\n *\n * Default representation example:\n *\n * ```md\n * **AC** 15 (10 with mage armor) note ability\n * ```\n *\n * @param value The AC value\n * @param alternateValues Alternate AC values as a map of (condition, AC value)\n * @param notes Any notes associated with the AC e.g. \"with mage armor\"\n * @param abilities Any AC related abilities\n */\n@TemplateData\npublic record QuteDataArmorClass(\n        Integer value, Map<String, Integer> alternateValues, List<String> notes,\n        List<String> abilities) implements QuteDataGenericStat {\n\n    public QuteDataArmorClass(Integer value) {\n        this(value, Map.of(), List.of(), List.of());\n    }\n\n    public QuteDataArmorClass(Integer value, Integer alternateValue) {\n        this(value, alternateValue == null ? Map.of() : Map.of(\"\", alternateValue), List.of(), List.of());\n    }\n\n    /**\n     * @param asBonus If true, then prefix alternate AC values with a +/-\n     * @return Alternate values formatted as e.g. {@code (30 with mage armor)}\n     */\n    private String formattedAlternates(boolean asBonus) {\n        String valFormat = asBonus ? \"%+d\" : \"%d\";\n        return join(\" \",\n                formatMap(alternateValues, (k, v) -> parenthesize(join(\" \", valFormat.formatted(v), k))));\n    }\n\n    @Override\n    public String bonus() {\n        return join(\" \", QuteDataGenericStat.super.bonus(), formattedAlternates(true));\n    }\n\n    @Override\n    public String toString() {\n        return flatJoin(\" \", List.of(\"**AC**\", value, formattedAlternates(false)), notes, abilities);\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataDefenses.java",
    "content": "package dev.ebullient.convert.tools.pf2e.qute;\n\nimport static dev.ebullient.convert.StringUtil.formatMap;\nimport static dev.ebullient.convert.StringUtil.join;\nimport static dev.ebullient.convert.StringUtil.joinWithPrefix;\nimport static dev.ebullient.convert.StringUtil.joiningNonEmpty;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.stream.Stream;\n\nimport dev.ebullient.convert.qute.QuteUtil;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteDataGenericStat.QuteDataNamedBonus;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * Pf2eTools Armor class, Saving Throws, and other attributes describing defenses of a creature or hazard.\n *\n * Example:\n *\n * ```md\n * **AC** 23 (33 with mage armor); **Fort** +15, **Ref** +12, **Will** +10\n * ```\n *\n * ```md\n * **Floor Hardness** 18, **Floor HP** 72 (BT 36);\n * **Channel Hardness** 12, **Channel HP** 48 (BT24 ) to destroy a channel gate;\n * **Immunities** critical hits;\n * **Resistances** precision damage;\n * **Weaknesses** bludgeoning damage\n * ```\n *\n * @param ac The armor class as a {@link QuteDataArmorClass}\n * @param savingThrows The saving throws, as {@link QuteDataDefenses.QuteSavingThrows}\n * @param hpHardnessBt HP, hardness, and broken threshold stored in a {@link QuteDataHpHardnessBt}\n * @param additionalHpHardnessBt Additional HP, hardness, or broken thresholds for other HP components as a map of\n *        names to {@link QuteDataHpHardnessBt}\n * @param immunities List of strings, optional\n * @param resistances Map of (name, {@link QuteDataGenericStat})\n * @param weaknesses Map of (name, {@link QuteDataGenericStat})\n */\n@TemplateData\npublic record QuteDataDefenses(\n        QuteDataArmorClass ac,\n        QuteSavingThrows savingThrows,\n        QuteDataHpHardnessBt hpHardnessBt,\n        Map<String, QuteDataHpHardnessBt> additionalHpHardnessBt,\n        List<String> immunities,\n        Map<String, QuteDataGenericStat> resistances,\n        Map<String, QuteDataGenericStat> weaknesses) implements QuteUtil {\n\n    @Override\n    public String toString() {\n        return join(\"\\n\",\n                // - **AC** 21; **Fort** +15, **Ref** +12, **Will** +10\n                joinWithPrefix(\"; \", \"- \", ac, savingThrows),\n                // - **Hardness** 18, **HP (BT)** 10; **Immunities** critical hits; **Resistances** fire 5\n                joinWithPrefix(\"; \", \"- \",\n                        hpHardnessBt,\n                        join(\"; \", formatMap(additionalHpHardnessBt, (k, v) -> v.toStringWithName(k))),\n                        joinWithPrefix(\", \", \"**Immunities** \", immunities),\n                        formatMap(resistances, (k, v) -> join(\" \", k, v)).stream().sorted()\n                                .collect(joiningNonEmpty(\", \", \"**Resistances** \")),\n                        formatMap(weaknesses, (k, v) -> join(\" \", k, v)).stream().sorted()\n                                .collect(joiningNonEmpty(\", \", \"**Weaknesses** \"))));\n    }\n\n    /**\n     * Pathfinder 2e saving throws. Example default rendering:\n     *\n     * ```md\n     * **Fort** +10 (+12 vs. poison), **Ref** +5 (+7 vs. traps), **Will** +4 (+6 vs. mental); +1 status to\n     * all saves vs. magic\n     * ```\n     *\n     * @param fort Fortitude saving throw bonus, as a {@link QuteDataGenericStat.QuteDataNamedBonus}\n     * @param ref Reflex saving throw bonus, as a {@link QuteDataGenericStat.QuteDataNamedBonus}\n     * @param will Will saving throw bonus, as a {@link QuteDataGenericStat.QuteDataNamedBonus}\n     * @param abilities Any saving throw related abilities\n     */\n    @TemplateData\n    public record QuteSavingThrows(\n            QuteDataNamedBonus fort, QuteDataNamedBonus ref, QuteDataNamedBonus will,\n            List<String> abilities) implements QuteUtil {\n        /** Returns all abilities as a formatted, comma-separated string. */\n        public String formattedAbilities() {\n            return join(\", \", abilities);\n        }\n\n        /** Returns all saving throws as a formatted string, not including any abilities. See class doc for example. */\n        public String formattedBonuses() {\n            return Stream.of(fort, ref, will)\n                    .filter(Objects::nonNull)\n                    .map(save -> \"**%s** %s\".formatted(save.name(), save.bonus()))\n                    .collect(joiningNonEmpty(\", \"));\n        }\n\n        @Override\n        public String toString() {\n            return join(\"; \", formattedBonuses(), formattedAbilities());\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataDuration.java",
    "content": "package dev.ebullient.convert.tools.pf2e.qute;\n\n/**\n * A duration of time. This may be either a {@link QuteDataTimedDuration}, which represents a period of time longer\n * than an activity, or a {@link QuteDataActivity}. Use {@link QuteDataDuration#isActivity()} to check whether this\n * duration is an activity.\n *\n * Using this directly will give the default representation for either object.\n */\npublic sealed interface QuteDataDuration permits QuteDataActivity, QuteDataTimedDuration {\n\n    /** Returns true if this duration is a {@link QuteDataActivity}. */\n    default boolean isActivity() {\n        return this instanceof QuteDataActivity;\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataFrequency.java",
    "content": "package dev.ebullient.convert.tools.pf2e.qute;\n\nimport static dev.ebullient.convert.StringUtil.join;\nimport static dev.ebullient.convert.StringUtil.numberAsWords;\nimport static dev.ebullient.convert.StringUtil.pluralize;\n\nimport java.util.List;\n\n/**\n * A description of a frequency e.g. \"once\", which may include an interval that this is repeated for.\n *\n * Examples:\n *\n * - once per day\n * - once per hour\n * - 3 times per day\n * - {@code recurs=true}: once every day\n * - {@code overcharge=true}: once per day, plus overcharge\n * - {@code interval=2}: once per 2 days\n *\n * @param value The number represented by the frequency, integer\n * @param unit The unit the frequency is in, string. Required.\n * @param recurs Whether the unit recurs. In the default representation, this makes it render \"every\" instead of \"per\"\n * @param overcharge Whether there's an overcharge involved. Used for wands mostly. In the default representation, this\n *        adds \", plus overcharge\".\n * @param interval The interval that the frequency is repeated for\n * @param notes Any notes associated with the frequency. May include a custom string, for frequencies which cannot be\n *        represented using the normal parts. If this is present, then the other parameters will be null.\n */\npublic record QuteDataFrequency(\n        Integer value, Integer interval, String unit, boolean recurs, boolean overcharge,\n        List<String> notes) implements QuteDataGenericStat {\n\n    public QuteDataFrequency(String special) {\n        this(null, null, null, false, false, List.of(special));\n    }\n\n    public QuteDataFrequency(\n            Integer value, Integer interval, String unit, boolean recurs, boolean overcharge) {\n        this(value, interval, unit, recurs, overcharge, List.of());\n    }\n\n    @Override\n    public String formattedNotes() {\n        return String.join(\", \", notes);\n    }\n\n    @Override\n    public String toString() {\n        if (!notes.isEmpty()) {\n            return formattedNotes();\n        }\n        return join(\" \",\n                switch (value) {\n                    case 1 -> \"once\";\n                    case 2 -> \"twice\";\n                    default -> \"%s times\".formatted(numberAsWords(value));\n                },\n                recurs ? \"every\" : \"per\",\n                interval,\n                pluralize(unit, interval == null ? 1 : interval, true)) + (overcharge ? \", plus overcharge\" : \"\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataGenericStat.java",
    "content": "package dev.ebullient.convert.tools.pf2e.qute;\n\nimport static dev.ebullient.convert.StringUtil.flatJoin;\nimport static dev.ebullient.convert.StringUtil.formatMap;\nimport static dev.ebullient.convert.StringUtil.join;\nimport static dev.ebullient.convert.StringUtil.joiningNonEmpty;\n\nimport java.util.List;\nimport java.util.Map;\n\nimport dev.ebullient.convert.StringUtil;\nimport dev.ebullient.convert.qute.QuteUtil;\nimport io.quarkus.qute.TemplateData;\n\n/** A generic container for a PF2e stat value which may have an attached note. */\npublic interface QuteDataGenericStat extends QuteUtil {\n    /** Returns the value of the stat. */\n    Integer value();\n\n    /** Returns any notes associated with this value. */\n    List<String> notes();\n\n    /** Return the value formatted with a leading +/-. */\n    default String bonus() {\n        return value() == null ? \"\" : \"%+d\".formatted(value());\n    }\n\n    /** Return notes formatted as space-delimited parenthesized strings. */\n    default String formattedNotes() {\n        return notes().stream().map(StringUtil::parenthesize).collect(joiningNonEmpty(\" \"));\n    }\n\n    /**\n     * A basic {@link dev.ebullient.convert.tools.pf2e.qute.QuteDataGenericStat QuteDataGenericStat} which provides\n     * only a value and possibly a note.\n     *\n     * Default representation: `10 (some note) (some other note)`\n     */\n    class SimpleStat implements QuteDataGenericStat {\n        private final Integer value;\n        private final List<String> notes;\n\n        public SimpleStat(Integer value, List<String> notes) {\n            this.value = value;\n            this.notes = notes;\n        }\n\n        public SimpleStat(Integer value) {\n            this(value, List.of());\n        }\n\n        public SimpleStat(Integer value, String note) {\n            this(value, note == null || note.isBlank() ? List.of() : List.of(note));\n        }\n\n        @Override\n        public String toString() {\n            return join(\" \", value, formattedNotes());\n        }\n\n        @Override\n        public Integer value() {\n            return value;\n        }\n\n        @Override\n        public List<String> notes() {\n            return notes;\n        }\n    }\n\n    /**\n     * A Pathfinder 2e named bonus, potentially with other conditional bonuses.\n     *\n     * Example default representation:\n     * `Stealth +36 (+42 in forests) (ignores tremorsense)`\n     *\n     * @param name The name of the skill\n     * @param value The standard bonus associated with this skill\n     * @param otherBonuses Any additional bonuses, as a map of descriptions to bonuses. Iterate over all map entries to\n     *        display the values, e.g.: {@code {#each resource.skills.otherBonuses}{it.key}: {it.value}{/each}}\n     * @param notes Any notes associated with this skill bonus\n     */\n    @TemplateData\n    record QuteDataNamedBonus(\n            String name, Integer value, Map<String, Integer> otherBonuses,\n            List<String> notes) implements QuteDataGenericStat {\n\n        public QuteDataNamedBonus(String name, Integer standardBonus) {\n            this(name, standardBonus, Map.of(), List.of());\n        }\n\n        /** Return the standard bonus and any other conditional bonuses. */\n        @Override\n        public String bonus() {\n            return flatJoin(\" \",\n                    List.of(QuteDataGenericStat.super.bonus()),\n                    formatMap(otherBonuses, (k, v) -> \"(%+d %s)\".formatted(v, k)));\n        }\n\n        @Override\n        public String toString() {\n            return join(\" \", name, bonus(), formattedNotes());\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataHpHardnessBt.java",
    "content": "package dev.ebullient.convert.tools.pf2e.qute;\n\nimport static dev.ebullient.convert.StringUtil.flatJoin;\nimport static dev.ebullient.convert.StringUtil.join;\n\nimport java.util.List;\nimport java.util.StringJoiner;\n\nimport dev.ebullient.convert.StringUtil;\nimport dev.ebullient.convert.qute.QuteUtil;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteDataGenericStat.SimpleStat;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * Hit Points, Hardness, and a broken threshold for hazards and shields. Used for creatures, hazards, and shields.\n *\n * Hazard example with a broken threshold and note:\n *\n * ```md\n * **Hardness** 10, **HP (BT)** 30 (15) to destroy a channel gate\n * ```\n *\n * Hazard example with a name, broken threshold, and note:\n *\n * ```md\n * **Floor Hardness** 10, **Floor HP** 30 (BT 15) to destroy a channel gate\n * ```\n *\n * Creature example with a name and ability:\n *\n * ```md\n * **Head Hardness** 10, **Head HP** 30 (hydra regeneration)\n * ```\n *\n * @param hp The HP as a {@link dev.ebullient.convert.tools.pf2e.qute.QuteDataHpHardnessBt.HpStat HpStat} (optional)\n * @param hardness Hardness as a {@link dev.ebullient.convert.tools.pf2e.qute.QuteDataGenericStat.SimpleStat SimpleStat}\n *        (optional)\n * @param brokenThreshold Broken threshold as an integer (optional, not populated for creatures)\n */\n@TemplateData\npublic record QuteDataHpHardnessBt(HpStat hp, SimpleStat hardness, Integer brokenThreshold) implements QuteUtil {\n\n    @Override\n    public String toString() {\n        return toStringWithName(\"\");\n    }\n\n    /** Return a representation of these stats with the given name used to label each component. */\n    public String toStringWithName(String name) {\n        name = name.isEmpty() ? \"\" : name + \" \";\n        StringJoiner parts = new StringJoiner(\", \");\n        if (hardness != null) {\n            parts.add(\"**%sHardness** %s\".formatted(name, hardness));\n        }\n        if (hp != null && hp.value != null) {\n            // If we have a BT but no name, then put the BT label next to the HP label. Otherwise, the BT label goes\n            // next to the BT value.\n            boolean labelBtWithHp = isPresent(brokenThreshold) && name.isEmpty();\n            parts.add(join(\" \",\n                    (labelBtWithHp ? \"**%sHP (BT)**\" : \"**%sHP**\").formatted(name),\n                    hp.value,\n                    isPresent(brokenThreshold) ? (labelBtWithHp ? \"(%d)\" : \"(BT %d)\").formatted(brokenThreshold) : null,\n                    hp.formattedNotes()));\n        }\n        return parts.toString();\n    }\n\n    /**\n     * HP value and associated notes. Referencing this directly provides a default representation, e.g.\n     * `15 to destroy a head (head regrowth)`\n     *\n     * @param value The HP value itself\n     * @param abilities Any abilities associated with the HP\n     * @param notes Any notes associated with the HP.\n     */\n    @TemplateData\n    public record HpStat(Integer value, List<String> notes, List<String> abilities) implements QuteDataGenericStat {\n        public HpStat(Integer value) {\n            this(value, null);\n        }\n\n        public HpStat(Integer value, String note) {\n            this(value, note == null || note.isBlank() ? List.of() : List.of(note), List.of());\n        }\n\n        @Override\n        public String toString() {\n            return join(\" \", value, formattedNotes());\n        }\n\n        /** Returns any notes and abilities formatted as a string. */\n        @Override\n        public String formattedNotes() {\n            return flatJoin(\" \",\n                    List.of(join(\", \", notes)),\n                    abilities.stream().map(StringUtil::parenthesize).toList());\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataRange.java",
    "content": "package dev.ebullient.convert.tools.pf2e.qute;\n\nimport static dev.ebullient.convert.StringUtil.join;\nimport static dev.ebullient.convert.StringUtil.pluralize;\n\nimport java.util.List;\n\n/**\n * A range with a given value and unit of measurement for that value.\n *\n * @param value An integer value for the range\n * @param unit What unit of measurement the {@code value} is given in, as a {@link QuteDataRange.RangeUnit}\n * @param notes Any associated notes, or an alternate rendering when the range can't be represented using just\n *        a unit and value.\n */\npublic record QuteDataRange(Integer value, RangeUnit unit, List<String> notes) implements QuteDataGenericStat {\n\n    public QuteDataRange(Integer value, RangeUnit unit, String note) {\n        this(value, unit, note == null || note.isBlank() ? List.of() : List.of(note));\n    }\n\n    @Override\n    public String formattedNotes() {\n        return join(\", \", notes);\n    }\n\n    @Override\n    public String toString() {\n        if (unit == null || unit == RangeUnit.UNKNOWN) {\n            return formattedNotes();\n        }\n        return switch (unit) {\n            case TOUCH, PLANETARY, INTERPLANAR, UNLIMITED -> unit.toString();\n            default -> join(\" \", value, pluralize(unit.toString(), value), formattedNotes());\n        };\n    }\n\n    public enum RangeUnit {\n        TOUCH,\n        FOOT,\n        MILE,\n        PLANETARY,\n        INTERPLANAR,\n        UNLIMITED,\n        UNKNOWN;\n\n        @Override\n        public String toString() {\n            return super.toString().toLowerCase();\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataSpeed.java",
    "content": "package dev.ebullient.convert.tools.pf2e.qute;\n\nimport static dev.ebullient.convert.StringUtil.flatJoin;\nimport static dev.ebullient.convert.StringUtil.formatMap;\nimport static dev.ebullient.convert.StringUtil.join;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\n\n/**\n * Examples:\n *\n * - `10 feet, swim 20 feet (some note); some ability`\n * - `10 feet, swim 20 feet, some ability`\n *\n * @param value The land speed in feet\n * @param otherSpeeds Other speeds, as a map of (name, speed in feet)\n * @param notes Any speed-related notes\n * @param abilities Any speed-related abilities\n */\npublic record QuteDataSpeed(\n        Integer value, Map<String, Integer> otherSpeeds, List<String> notes,\n        List<String> abilities) implements QuteDataGenericStat {\n\n    public void addAbility(String ability) {\n        abilities.add(ability);\n    }\n\n    /** Return formatted notes and abilities. e.g. {@code (note) (another note); ability, another ability} */\n    @Override\n    public String formattedNotes() {\n        return join(\"; \", QuteDataGenericStat.super.formattedNotes(), join(\", \", abilities));\n    }\n\n    /** Return formatted speeds as a string, starting with land speed. e.g. {@code 10 feet, swim 20 feet} */\n    public String formattedSpeeds() {\n        return flatJoin(\", \",\n                List.of(Optional.ofNullable(value).map(\"%d feet\"::formatted).orElse(\"no land speed\")),\n                formatMap(otherSpeeds, \"%s %d feet\"::formatted));\n    }\n\n    @Override\n    public String toString() {\n        return join(notes.isEmpty() ? \", \" : \" \", formattedSpeeds(), formattedNotes());\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDataTimedDuration.java",
    "content": "package dev.ebullient.convert.tools.pf2e.qute;\n\nimport static dev.ebullient.convert.StringUtil.join;\nimport static dev.ebullient.convert.StringUtil.pluralize;\n\nimport java.util.List;\n\nimport dev.ebullient.convert.io.JavadocVerbatim;\n\n/**\n * A duration of time, represented by a numerical value and a unit. Sometimes this includes a custom display string,\n * for durations which cannot be represented using the normal structure.\n *\n * Examples:\n *\n * - A duration of 3 minutes: `3 minutes`\n * - A duration of 1 turn: `until the end of your next turn`\n * - An unlimited duration: `unlimited`\n *\n * @param value The quantity of time\n * @param unit The unit that the quantity is measured in, as a {@link QuteDataTimedDuration.DurationUnit}\n */\npublic record QuteDataTimedDuration(Integer value, DurationUnit unit,\n        List<String> notes) implements QuteDataGenericStat, QuteDataDuration {\n\n    public QuteDataTimedDuration(Integer value, DurationUnit unit, String note) {\n        this(value, unit, note == null || note.isBlank() ? List.of() : List.of(note));\n    }\n\n    /** The custom display used for this duration. */\n    public String getCustomDisplay() {\n        return formattedNotes();\n    }\n\n    /** Returns true if we use a custom display string to show this instead of the value and unit. */\n    public boolean hasCustomDisplay() {\n        return !notes.isEmpty();\n    }\n\n    /** Returns a comma delimited string containing all notes. */\n    @JavadocVerbatim\n    @Override\n    public String formattedNotes() {\n        return join(\", \", notes);\n    }\n\n    @Override\n    public String toString() {\n        if (hasCustomDisplay()) {\n            return getCustomDisplay();\n        }\n        if (unit == DurationUnit.UNLIMITED) {\n            return \"unlimited\";\n        }\n        if (value != null && value == 1 && unit == DurationUnit.TURN) {\n            return \"until the end of your next turn\";\n        }\n        return join(\" \", value, pluralize(unit == null ? null : unit.toString(), value));\n    }\n\n    /** Represents different units that a duration might be in. */\n    public enum DurationUnit {\n        REACTION,\n        ACTION,\n        TURN,\n        ROUND,\n        MINUTE,\n        HOUR,\n        DAY,\n        MONTH,\n        UNLIMITED,\n        SPECIAL;\n\n        @Override\n        public String toString() {\n            return super.toString().toLowerCase();\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteDeity.java",
    "content": "package dev.ebullient.convert.tools.pf2e.qute;\n\nimport static dev.ebullient.convert.StringUtil.flatJoin;\nimport static dev.ebullient.convert.StringUtil.join;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\nimport dev.ebullient.convert.qute.NamedText;\nimport dev.ebullient.convert.qute.QuteUtil;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.Pf2eSources;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * Pf2eTools Deity attributes ({@code deity2md.txt})\n *\n * Deities are rendered both standalone and inline (as an admonition block).\n * The default template can render both.\n * It uses special syntax to handle the inline case.\n *\n * Use `%%--` to mark the end of the preamble (frontmatter and\n * other leading content only appropriate to the standalone case).\n *\n * Extension of {@link dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase Pf2eQuteBase}\n */\n@TemplateData\npublic class QuteDeity extends Pf2eQuteBase {\n\n    private final List<String> altNames;\n    public final String category;\n    public final String pantheon;\n\n    // Morality\n    public final String alignment;\n    public final String followerAlignment;\n\n    public final String areasOfConcern;\n    public final String edicts;\n    public final String anathema;\n\n    public final QuteDeityCleric cleric;\n    public final QuteDivineAvatar avatar;\n    public final QuteDivineIntercession intercession;\n\n    public QuteDeity(Pf2eSources sources, List<String> text, Tags tags,\n            List<String> aliases, String category, String pantheon,\n            String alignment, String followerAlignment, String areasOfConcern, String edicts, String anathema,\n            QuteDeityCleric cleric, QuteDivineAvatar avatar, QuteDivineIntercession intercession) {\n        super(sources, text, tags);\n        this.altNames = aliases;\n        this.category = category;\n        this.pantheon = pantheon;\n\n        this.alignment = alignment;\n        this.followerAlignment = followerAlignment;\n        this.areasOfConcern = areasOfConcern;\n        this.edicts = edicts;\n        this.anathema = anathema;\n\n        this.cleric = cleric;\n        this.avatar = avatar;\n        this.intercession = intercession;\n    }\n\n    public List<String> getAltNames() {\n        // Used by getAliases in QuteBase\n        return altNames;\n    }\n\n    /**\n     * Pf2eTools cleric divine attributes\n     *\n     *\n     * This data object provides a default mechanism for creating\n     * a marked up string based on the attributes that are present.\n     * To use it, reference it directly: `{resource.actionType}`.\n     *\n     */\n    @TemplateData\n    public static class QuteDeityCleric implements QuteUtil {\n        public String divineFont;\n        public String divineAbility;\n        public String divineSkill;\n        public String domains;\n        public String alternateDomains;\n        public Map<String, String> spells;\n        public String favoredWeapon;\n\n        public String toString() {\n            List<String> lines = new ArrayList<>();\n            if (isPresent(divineAbility)) {\n                lines.add(\"- **Divine Ability**: \" + divineAbility);\n            }\n            if (isPresent(divineFont)) {\n                lines.add(\"- **Divine Font**: \" + divineFont);\n            }\n            if (isPresent(divineSkill)) {\n                lines.add(\"- **Divine Skill**: \" + divineSkill);\n            }\n            if (isPresent(favoredWeapon)) {\n                lines.add(\"- **Favored Weapon**: \" + favoredWeapon);\n            }\n            if (domains != null && !domains.isEmpty()) {\n                lines.add(\"- **Domains**: \" + domains);\n            }\n            if (alternateDomains != null && !alternateDomains.isEmpty()) {\n                lines.add(\"- **Alternate Domains**: \" + alternateDomains);\n            }\n            if (isPresent(spells)) {\n                lines.add(\"- **Cleric Spells**: \" + spells.entrySet().stream()\n                        .map(e -> String.format(\"%s: %s\", e.getKey(), e.getValue()))\n                        .collect(Collectors.joining(\"; \")));\n            }\n            return String.join(\"\\n\", lines);\n        }\n    }\n\n    /**\n     * Pf2eTools avatar attributes\n     *\n     *\n     * This data object provides a default mechanism for creating\n     * a marked up string based on the attributes that are present.\n     * To use it, reference it directly: `{resource.actionType}`.\n     *\n     */\n    @TemplateData\n    public static class QuteDivineAvatar implements QuteUtil {\n        public String preface;\n        public String name;\n        /** The avatar's speed, as a {@link dev.ebullient.convert.tools.pf2e.qute.QuteDataSpeed QuteDataSpeed} */\n        public QuteDataSpeed speed;\n        public List<String> abilities;\n        public String shield;\n        public List<QuteInlineAttack> attacks;\n        public Collection<NamedText> ability;\n\n        /**\n         * Example:\n         *\n         * ```md\n         * **Cee-el-aye** When casting the <i>avatar</i> spell, a worshipper of the Cee-el-aye typically begins reading\n         * entirely too much JSON, and gains the following additional abilities.\n         *\n         * Speed 50 feet, burrow 70 feet, immune to <u>petrified</u>;\n         * shield (15 Hardness, can't be damaged);\n         * **Melee** polytool (<u>reach 15 feet</u>), **Damage** 6d6+6 slashing;\n         * **Ranged** pull request (<u>nonlethal</u>, <u>reach 9358 miles</u>), **Damage** 3d6+3 mental plus commit\n         * history;\n         * **Commit History** A creature who reviews the pull request must spend the next 1d4 hours reading code.\n         * ```\n         */\n        @Override\n        public String toString() {\n            String speedText = speed == null\n                    ? \"\"\n                    : join(\", \", \"Speed %s\".formatted(speed.formattedSpeeds()), speed.formattedNotes());\n            return \"**\" + name + \"** \" + flatJoin(\"; \", List.of(speedText, shield), attacks, ability);\n        }\n    }\n\n    /**\n     * Pf2eTools divine intercession attributes.\n     *\n     *\n     * This data object provides a default mechanism for creating\n     * a marked up string based on the attributes that are present.\n     * To use it, reference it directly: `{resource.actionType}`.\n     *\n     */\n    @TemplateData\n    public static class QuteDivineIntercession {\n        public String source;\n        public String flavor;\n        public String majorBoon;\n        public String moderateBoon;\n        public String minorBoon;\n        public String majorCurse;\n        public String moderateCurse;\n        public String minorCurse;\n\n        @Deprecated\n        public String getSourceText() {\n            return source;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteFeat.java",
    "content": "package dev.ebullient.convert.tools.pf2e.qute;\n\nimport java.util.Collection;\nimport java.util.List;\n\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.Pf2eSources;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * Pf2eTools Feat attributes ({@code feat2md.txt})\n *\n * Feats are rendered both standalone and inline (as an admonition block).\n * The default template can render both.\n * It uses special syntax to handle the inline case.\n *\n * Use `%%--` to mark the end of the preamble (frontmatter and\n * other leading content only appropriate to the standalone case).\n *\n * Extension of {@link dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase Pf2eQuteBase}\n */\n@TemplateData\npublic class QuteFeat extends Pf2eQuteBase {\n\n    /** Collection of traits (decorated links) */\n    public final Collection<String> traits;\n\n    private final List<String> altNames;\n\n    public final String level;\n    public final String access;\n    /**\n     * {@link dev.ebullient.convert.tools.pf2e.qute.QuteDataFrequency QuteDataFrequency}.\n     * How often this feat can be used/activated. Use directly to get a formatted string.\n     */\n    public final QuteDataFrequency frequency;\n    /** Activity/Activation cost (as {@link dev.ebullient.convert.tools.pf2e.qute.QuteDataActivity QuteDataActivity}) */\n    public final QuteDataActivity activity;\n    public final String trigger;\n    public final String cost;\n    public final String requirements;\n    public final String prerequisites;\n    public final String special;\n    public final List<String> leadsTo;\n    public final String note;\n    /**\n     * True if this ability is embedded in another note (admonition block).\n     * The default template uses this flag to include a `title:` prefix for the admonition block:<br />\n     * `{#if resource.embedded }title: {#else}# {/if}{resource.name}` *\n     */\n    public final boolean embedded;\n\n    public QuteFeat(Pf2eSources sources, List<String> text, Tags tags,\n            Collection<String> traits, List<String> aliases,\n            String level, String access, QuteDataFrequency frequency, QuteDataActivity activity, String trigger,\n            String cost, String requirements, String prerequisites, String special, String note,\n            List<String> leadsTo, boolean embedded) {\n        super(sources, text, tags);\n        this.traits = traits;\n        this.altNames = aliases;\n\n        this.level = level;\n        this.access = access;\n        this.frequency = frequency;\n        this.activity = activity;\n        this.trigger = trigger;\n        this.cost = cost;\n        this.requirements = requirements;\n        this.prerequisites = prerequisites;\n        this.special = special;\n        this.note = note;\n        this.leadsTo = leadsTo;\n        this.embedded = embedded;\n    }\n\n    public List<String> getAltNames() {\n        // Used by getAliases in QuteBase\n        return altNames;\n    }\n\n    @Override\n    public boolean getHasSections() {\n        return super.getHasSections() || (leadsTo != null && !leadsTo.isEmpty());\n    }\n\n    public String title() {\n        return String.format(\"%s%s, *Feat %s*\", getName(),\n                activity == null\n                        ? \"\"\n                        : \" \" + activity,\n                level);\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteHazard.java",
    "content": "package dev.ebullient.convert.tools.pf2e.qute;\n\nimport static dev.ebullient.convert.StringUtil.join;\nimport static dev.ebullient.convert.StringUtil.parenthesize;\n\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.List;\n\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.Pf2eSources;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * Pf2eTools Hazard attributes ({@code hazard2md.txt})\n *\n * Hazards are rendered both standalone and inline (as an admonition block).\n * The default template can render both.\n * It uses special syntax to handle the inline case.\n *\n * Use `%%--` to mark the end of the preamble (frontmatter and\n * other leading content only appropriate to the standalone case).\n *\n * Extension of {@link dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase Pf2eQuteBase}\n */\n@TemplateData\npublic class QuteHazard extends Pf2eQuteBase {\n\n    /** Collection of traits (decorated links) */\n    public final Collection<String> traits;\n\n    public final String level;\n    public final String disable;\n    public final String reset;\n    public final String routine;\n    public final QuteDataDefenses defenses;\n\n    /**\n     * The attacks available to the hazard, as a list of\n     * {@link dev.ebullient.convert.tools.pf2e.qute.QuteInlineAttack QuteInlineAttack}\n     */\n    public final List<QuteInlineAttack> attacks;\n\n    /**\n     * The hazard's abilities, as a list of\n     * {@link dev.ebullient.convert.tools.pf2e.qute.QuteAbility QuteAbility}\n     */\n    public final List<QuteAbility> abilities;\n\n    /**\n     * The hazard's actions, as a list of\n     * {@link dev.ebullient.convert.tools.pf2e.qute.QuteAbilityOrAffliction QuteAbilityOrAffliction}.\n     *\n     * Using the elements directly will give a default rendering, but if you want more\n     * control you can use {@code isAffliction} and {@code isAbility} to check whether it's an affliction or an\n     * ability. Example:\n     *\n     *\n     * ```md\n     * {#each resource.actions}\n     * {#if it.isAffliction}\n     *\n     * **Affliction** {it}\n     * {#else if it.isAbility}\n     *\n     * **Ability** {it}\n     * {/if}\n     * {/each}\n     * ```\n     */\n    public final List<QuteAbilityOrAffliction> actions;\n\n    /**\n     * The hazard's stealth, as a\n     * {@link dev.ebullient.convert.tools.pf2e.qute.QuteHazard.QuteHazardStealth QuteHazardAttributes}\n     */\n    public final QuteHazardStealth stealth;\n\n    /**\n     * The hazard's perception, as a\n     * {@link dev.ebullient.convert.tools.pf2e.qute.QuteDataGenericStat QuteDataGenericStat}\n     */\n    public final QuteDataGenericStat perception;\n\n    public QuteHazard(Pf2eSources sources, List<String> text, Tags tags,\n            Collection<String> traits, String level, String disable,\n            String reset, String routine, QuteDataDefenses defenses,\n            List<QuteInlineAttack> attacks, List<QuteAbility> abilities, List<QuteAbilityOrAffliction> actions,\n            QuteHazardStealth stealth, QuteDataGenericStat perception) {\n        super(sources, text, tags);\n        this.traits = traits;\n        this.level = level;\n        this.reset = reset;\n        this.routine = routine;\n        this.disable = disable;\n        this.attacks = attacks;\n        this.abilities = abilities;\n        this.actions = actions;\n        this.defenses = defenses;\n        this.stealth = stealth;\n        this.perception = perception;\n    }\n\n    public String getComplexity() {\n        if (traits == null || traits.stream().noneMatch(t -> t.contains(\"complex\"))) {\n            return \"Simple\";\n        }\n        return \"Complex\";\n    }\n\n    public String getRoutineAdmonition() {\n        return convertToEmbed(routine, \"Routine\", \"pf2-summary\");\n    }\n\n    public String convertToEmbed(String content, String title, String admonition) {\n        int embedDepth = Arrays.stream(content.split(\"\\n\"))\n                .filter(s -> s.matches(\"^`+$\"))\n                .map(String::length)\n                .max(Integer::compare).orElse(2);\n        char[] ticks = new char[embedDepth + 1];\n        Arrays.fill(ticks, '`');\n        String backticks = new String(ticks);\n\n        return backticks + \"ad-\" + admonition + \"\\n\"\n                + \"title: \" + title + \"\\n\\n\"\n                + content + \"\\n\"\n                + backticks;\n    }\n\n    /**\n     * Pf2eTools hazard attributes.\n     *\n     * This data object provides a default mechanism for creating\n     * a marked up string based on the attributes that are present.\n     *\n     * @param value The hazard's Stealth bonus\n     * @param minProf The minimum Perception proficiency required to be able to roll against the hazard's Stealth\n     * @param notes Any notes associated with the hazard's Stealth. Sometimes this includes other stats which may\n     *        be rolled against the hazard's Stealth.\n     * @param dc The DC which must be passed to see the hazard\n     */\n    @TemplateData\n    public record QuteHazardStealth(\n            Integer value, Integer dc, String minProf, List<String> notes) implements QuteDataGenericStat {\n\n        public QuteHazardStealth(Integer value, Integer dc, String minProf, String note) {\n            this(value, dc, minProf, note == null || note.isEmpty() ? List.of() : List.of(note));\n        }\n\n        @Override\n        public String formattedNotes() {\n            return join(\" \", parenthesize(minProf), join(\", \", notes));\n        }\n\n        @Override\n        public String toString() {\n            return join(\" \", bonus(), dc != null ? \"DC \" + dc : \"\", formattedNotes());\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteInlineAttack.java",
    "content": "package dev.ebullient.convert.tools.pf2e.qute;\n\nimport static dev.ebullient.convert.StringUtil.join;\nimport static dev.ebullient.convert.StringUtil.parenthesize;\nimport static dev.ebullient.convert.StringUtil.toTitleCase;\n\nimport java.util.Collection;\nimport java.util.List;\n\nimport dev.ebullient.convert.qute.QuteUtil;\nimport dev.ebullient.convert.tools.JsonTextConverter;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * Pf2eTools Attack attributes (inline/embedded, {@code inline-attack2md.txt})\n *\n * When used directly, renders according to {@code inline-attack2md.txt}\n */\n@TemplateData\npublic final class QuteInlineAttack implements QuteDataGenericStat, QuteUtil.Renderable {\n\n    /** The name of the attack e.g. \"fist\" (string) */\n    public final String name;\n\n    /** Number/type of action cost ({@link QuteDataActivity QuteDataActivity}) */\n    public final QuteDataActivity activity;\n\n    /**\n     * The range of the attack ({@link QuteInlineAttack.AttackRangeType AttackRangeType} enum)\n     */\n    public final AttackRangeType rangeType;\n\n    /** The to-hit bonus for the attack (integer) */\n    public final Integer attackBonus;\n\n    /**\n     * Damage if the attack hits (formatted string), e.g. \"1d8 bludgeoning plus grab\". This will include\n     * damage types and non-multiline effects.\n     */\n    public final String damage;\n\n    /**\n     * The damage types caused by the attack. Will be included in either\n     * {@link QuteInlineAttack#damage damage} or in\n     * {@link QuteInlineAttack#multilineEffect multilineEffect}.\n     */\n    public final Collection<String> damageTypes;\n\n    /** Any traits associated with the attack (collection of decorated links) */\n    public final Collection<String> traits;\n\n    /**\n     * Any additional effects associated with the attack e.g. grab (list of strings). Effects listed here\n     * may be repeated in {@link QuteInlineAttack#damage damage}.\n     */\n    public final List<String> effects;\n\n    /** A multi-line effect. Formatted string, will be null if there is no multiline effect. */\n    public final String multilineEffect;\n\n    /** Any notes associated with the attack e.g. \"no multiple attack penalty\" (list of strings) */\n    public final List<String> notes;\n\n    // Internal use only\n    private final JsonTextConverter<?> _converter;\n\n    public QuteInlineAttack(\n            String name, QuteDataActivity activity, AttackRangeType rangeType, Integer attackBonus, String damage,\n            Collection<String> damageTypes, Collection<String> traits, List<String> effects, String multilineEffect,\n            List<String> notes, JsonTextConverter<?> converter) {\n        this.name = name;\n        this.activity = activity;\n        this.rangeType = rangeType;\n        this.attackBonus = attackBonus;\n        this.damage = damage;\n        this.damageTypes = damageTypes;\n        this.traits = traits;\n        this.effects = effects;\n        this.multilineEffect = multilineEffect;\n        this.notes = notes;\n        this._converter = converter;\n    }\n\n    public QuteInlineAttack(\n            String name, QuteDataActivity activity, AttackRangeType rangeType, String damage,\n            Collection<String> damageTypes, Collection<String> traits, String note,\n            JsonTextConverter<?> converter) {\n        this(\n                name, activity, rangeType, null,\n                damage, damageTypes, traits,\n                List.of(), null, note == null ? List.of() : List.of(note), converter);\n    }\n\n    @Override\n    public List<String> notes() {\n        return notes;\n    }\n\n    @Override\n    public Integer value() {\n        return attackBonus;\n    }\n\n    @Override\n    public String template() {\n        return \"inline-attack2md.txt\";\n    }\n\n    @Override\n    public String render() {\n        return _converter.renderInlineTemplate(this, null);\n    }\n\n    @Override\n    public String toString() {\n        return render();\n    }\n\n    /** Return traits formatted as a single string, e.g. {@code (agile, trip, finesse)} */\n    public String formattedTraits() {\n        return parenthesize(join(\", \", traits));\n    }\n\n    public enum AttackRangeType {\n        RANGED,\n        MELEE;\n\n        @Override\n        public String toString() {\n            return toTitleCase(name());\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteItem.java",
    "content": "package dev.ebullient.convert.tools.pf2e.qute;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\nimport dev.ebullient.convert.qute.NamedText;\nimport dev.ebullient.convert.qute.QuteUtil;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.Pf2eSources;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * Pf2eTools Item attributes\n *\n * Extension of {@link dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase Pf2eQuteBase}\n */\n@TemplateData\npublic class QuteItem extends Pf2eQuteBase {\n\n    private final List<String> altNames;\n\n    /** Collection of traits (decorated links) */\n    public final Collection<String> traits;\n    /**\n     * Item activation attributes as {@link dev.ebullient.convert.tools.pf2e.qute.QuteItem.QuteItemActivate QuteItemActivate}\n     */\n    public final QuteItemActivate activate;\n    /** Formatted string. Item price (pp, gp, sp, cp) */\n    public final String price;\n    /** Formatted string. Crafting requirements */\n    public final String craftReq;\n    /** Formatted string. Ammunition required */\n    public final String ammunition;\n    /** Formatted string. Onset attributes */\n    public final String onset;\n    /** Formatted string. Item power level */\n    public final String level;\n    /** Formatted string. Item access attributes */\n    public final String access;\n    /** Formatted string. How long will the item remain active */\n    public final String duration;\n    /** Formatted string. Item category */\n    public final String category;\n    /** Formatted string. Item group */\n    public final String group;\n    /** Formatted string. How many hands does this item require to use */\n    public final String hands;\n    /** Item use attributes as a list of {@link dev.ebullient.convert.qute.NamedText NamedText} */\n    public final Collection<NamedText> usage;\n    /** Item contract attributes as a list of {@link dev.ebullient.convert.qute.NamedText NamedText} */\n    public final Collection<NamedText> contract;\n    /**\n     * Item shield attributes as {@link dev.ebullient.convert.tools.pf2e.qute.QuteItem.QuteItemShieldData QuteItemShieldData}\n     */\n    public final QuteItemShieldData shield;\n    /** Item armor attributes as {@link dev.ebullient.convert.tools.pf2e.qute.QuteItem.QuteItemArmorData QuteItemArmorData} */\n    public final QuteItemArmorData armor;\n    /**\n     * Item weapon attributes as list of {@link dev.ebullient.convert.tools.pf2e.qute.QuteItem.QuteItemWeaponData\n     * QuteItemWeaponData}\n     */\n    public final List<QuteItemWeaponData> weapons;\n    /** Item variants as list of {@link dev.ebullient.convert.tools.pf2e.qute.QuteItem.QuteItemVariant QuteItemVariant} */\n    public final List<QuteItemVariant> variants;\n\n    public QuteItem(Pf2eSources sources, List<String> text, Tags tags,\n            Collection<String> traits, List<String> aliases, QuteItemActivate activate,\n            String price, String ammunition, String level, String onset, String access,\n            String duration, String category, String group,\n            String hands, Collection<NamedText> usage, Collection<NamedText> contract,\n            QuteItemShieldData shield, QuteItemArmorData armor, List<QuteItemWeaponData> weapons,\n            List<QuteItemVariant> variants, String craftReq) {\n        super(sources, text, tags);\n        this.traits = traits;\n        this.altNames = aliases;\n\n        this.activate = activate;\n        this.price = price;\n        this.ammunition = ammunition;\n        this.level = level;\n        this.onset = onset;\n        this.access = access;\n        this.duration = duration;\n        this.category = category;\n        this.group = group;\n        this.usage = usage;\n        this.hands = hands;\n        this.contract = contract;\n        this.shield = shield;\n        this.armor = armor;\n        this.weapons = weapons;\n        this.variants = variants;\n        this.craftReq = craftReq;\n    }\n\n    @Override\n    public List<String> getAltNames() {\n        // Used by getAliases in QuteBase\n        return altNames;\n    }\n\n    /**\n     * Pf2eTools item activation attributes.\n     *\n     * This data object provides a default mechanism for creating\n     * a marked up string based on the attributes that are present.\n     * To use it, reference it directly: `{resource.activate}`.\n     */\n    @TemplateData\n    public static class QuteItemActivate implements QuteUtil {\n        /** Item {@link dev.ebullient.convert.tools.pf2e.qute.QuteDataActivity activity/activation details} */\n        public QuteDataActivity activity;\n        /** Formatted string. Components required to activate this item */\n        public String components;\n        /** Formatted string. Trigger to activate this item */\n        public String trigger;\n        /**\n         * {@link dev.ebullient.convert.tools.pf2e.qute.QuteDataFrequency QuteDataFrequency}.\n         * How often this item can be used/activated. Use directly to get a formatted string.\n         */\n        public QuteDataFrequency frequency;\n        /** Formatted string. Requirements for activating this item */\n        public String requirements;\n\n        public String toString() {\n            List<String> lines = new ArrayList<>();\n            if (activity != null || isPresent(components)) {\n                lines.add(String.join(\" \", List.of(\n                        activity == null ? \"\" : activity.toString(),\n                        components == null ? \"\" : components)).trim());\n            }\n            if (isPresent(frequency)) {\n                lines.add(\"**Frequency** \" + frequency);\n            }\n            if (isPresent(trigger)) {\n                lines.add(\"**Trigger** \" + trigger);\n            }\n            if (isPresent(requirements)) {\n                lines.add(\"**Requirements** \" + requirements);\n            }\n\n            return String.join(\"; \", lines);\n        }\n    }\n\n    /**\n     * Pf2eTools item shield attributes. When referenced directly, provides a default formatting, e.g.\n     *\n     * ```md\n     * **AC Bonus** +2; **Speed Penalty** —; **Hardness** 3; **HP (BT)** 12 (6)\n     * ```\n     *\n     * @param ac AC bonus for the shield, as {@link dev.ebullient.convert.tools.pf2e.qute.QuteDataArmorClass QuteDataArmorClass}\n     *        (required)\n     * @param hpHardnessBt HP, hardness, and broken threshold of the shield, as\n     *        {@link dev.ebullient.convert.tools.pf2e.qute.QuteDataHpHardnessBt QuteDataHpHardnessBt}\n     *        (required)\n     * @param speedPenalty Speed penalty for the shield, as a formatted string (string, required)\n     */\n    @TemplateData\n    public record QuteItemShieldData(\n            QuteDataArmorClass ac,\n            QuteDataHpHardnessBt hpHardnessBt,\n            String speedPenalty) implements QuteUtil {\n\n        @Override\n        public String toString() {\n            return String.join(\"; \",\n                    \"**AC Bonus** \" + ac.bonus(), \"**Speed Penalty** \" + speedPenalty, hpHardnessBt.toString());\n        }\n    }\n\n    /**\n     * Pf2eTools item armor attributes\n     *\n     * This data object provides a default mechanism for creating\n     * a marked up string based on the attributes that are present.\n     * To use it, reference it directly: `{resource.armor}`.\n     */\n    @TemplateData\n    public static class QuteItemArmorData implements QuteUtil {\n        /** {@link dev.ebullient.convert.tools.pf2e.qute.QuteDataArmorClass Item armor class details} */\n        public QuteDataArmorClass ac;\n        /** Formatted string. Dex cap */\n        public String dexCap;\n        /** Formatted string. Armor strength */\n        public String strength;\n        /** Formatted string. Check penalty */\n        public String checkPenalty;\n        /** Formatted string. Speed penalty */\n        public String speedPenalty;\n\n        public String toString() {\n            List<String> parts = new ArrayList<>();\n            parts.add(\"**AC Bonus** \" + ac.bonus());\n            if (isPresent(dexCap)) {\n                parts.add(\"**Dex Cap** \" + dexCap);\n            }\n            if (isPresent(strength)) {\n                parts.add(\"**Strength** \" + strength);\n            }\n            if (isPresent(checkPenalty)) {\n                parts.add(\"**Check Penalty** \" + checkPenalty);\n            }\n            if (isPresent(speedPenalty)) {\n                parts.add(\"**Speed Penalty** \" + speedPenalty);\n            }\n            return \"- \" + String.join(\"; \", parts);\n        }\n    }\n\n    /**\n     * Pf2eTools item weapon attributes\n     *\n     * This data object provides a default mechanism for creating\n     * a marked up string based on the attributes that are present.\n     *\n     * To use it, reference it directly:\n     *\n     * ```md\n     * {#for weapons in resource.weapons}\n     * {weapons}\n     * {/for}\n     * ```\n     *\n     * or, using `{#each}` instead:\n     *\n     * ```md\n     * {#each resource.weapons}\n     * {it}\n     * {/each}\n     * ```\n     */\n    @TemplateData\n    public static class QuteItemWeaponData implements QuteUtil {\n        /** Formatted string. Weapon type */\n        public String type;\n        /** Formatted string. List of traits (links) */\n        public Collection<String> traits;\n        public Collection<NamedText> ranged;\n        public String damage;\n        public String group;\n\n        public String toString() {\n            String result = \"\";\n            String prefix = type == null ? \"\" : \"  \";\n\n            if (isPresent(type)) {\n                result += \"- **\" + type + \"**:  \\n\";\n            }\n            if (isPresent(damage)) {\n                result += prefix + \"- **Damage**: \" + damage;\n            }\n            if (isPresent(ranged)) {\n                if (isPresent(damage)) {\n                    result += \"\\n\";\n                }\n                result += prefix + \"- \" + ranged.stream()\n                        .map(e -> e.toString())\n                        .collect(Collectors.joining(\"; \"));\n            }\n            return result;\n        }\n    }\n\n    /**\n     * Pf2eTools item variant attributes\n     *\n     * This data object provides a default mechanism for creating\n     * a marked up string based on the attributes that are present.\n     *\n     * To use it, reference it directly:\n     *\n     * ```md\n     * {#for variants in resource.variants}\n     * {variants}\n     * {/for}\n     * ```\n     *\n     * or, using `{#each}` instead:\n     *\n     * ```md\n     * {#each resource.variants}\n     * {it}\n     * {/each}\n     * ```\n     */\n    @TemplateData\n    public static class QuteItemVariant implements QuteUtil {\n        public String variantType;\n        public int level;\n        public String price;\n        public List<String> entries;\n        public List<String> craftReq;\n\n        public String toString() {\n            List<String> text = new ArrayList<>();\n            text.add(String.format(\"#### %s *Item %d*\", variantType, level));\n            text.add(\"\");\n            if (isPresent(price)) {\n                text.add(String.format(\"- **Price**: %s\", price));\n            }\n            if (isPresent(craftReq)) {\n                text.add(\"- **Craft Requirements**: \" +\n                        String.join(\"; \", craftReq));\n            }\n            String bodyText = String.join(\"\\n\", this.entries);\n            if (!bodyText.isBlank()) {\n                text.add(\"\");\n                text.add(bodyText);\n            }\n\n            return String.join(\"\\n\", text);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteRitual.java",
    "content": "package dev.ebullient.convert.tools.pf2e.qute;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\n\nimport dev.ebullient.convert.qute.NamedText;\nimport dev.ebullient.convert.qute.QuteUtil;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.Pf2eSources;\nimport dev.ebullient.convert.tools.pf2e.qute.QuteSpell.QuteSpellTarget;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * Pf2eTools Ritual attributes ({@code ritual2md.txt})\n *\n * Extension of {@link dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase Pf2eQuteBase}\n */\n@TemplateData\npublic class QuteRitual extends Pf2eQuteBase {\n\n    private final List<String> altNames;\n\n    /** A spell’s overall power, from 1 to 10. */\n    public final String level;\n    /** Type: Ritual (usually) */\n    public final String ritualType;\n    /** Collection of traits (decorated links) */\n    public final Collection<String> traits;\n\n    /** Casting attributes as {@link dev.ebullient.convert.tools.pf2e.qute.QuteRitual.QuteRitualCasting QuteRitualCasting} */\n    public final QuteRitualCasting casting;\n    /** Casting attributes as {@link dev.ebullient.convert.tools.pf2e.qute.QuteRitual.QuteRitualChecks QuteRitualChecks} */\n    public final QuteRitualChecks checks;\n    /** Casting attributes as {@link dev.ebullient.convert.tools.pf2e.qute.QuteSpell.QuteSpellTarget QuteSpellTarget} */\n    public final QuteSpellTarget targeting;\n\n    /** Formatted text. Ritual requirements */\n    public final String requirements;\n    /** Formated text. Ritual duration */\n    public final String duration;\n\n    /** Heightened spell effects as a list of {@link dev.ebullient.convert.qute.NamedText Traits} */\n    public final Collection<NamedText> heightened;\n\n    public QuteRitual(Pf2eSources sources, List<String> text, Tags tags,\n            String level, String ritualType, Collection<String> traits, List<String> aliases,\n            QuteRitualCasting casting, QuteRitualChecks checks, QuteSpellTarget targeting,\n            String requirements, String duration, Collection<NamedText> heightened) {\n        super(sources, text, tags);\n\n        this.level = level;\n        this.ritualType = ritualType;\n        this.traits = traits;\n        this.altNames = aliases;\n        this.casting = casting;\n        this.checks = checks;\n        this.targeting = targeting;\n        this.requirements = requirements;\n        this.duration = duration;\n        this.heightened = heightened;\n    }\n\n    @Override\n    public List<String> getAltNames() {\n        // Used by getAliases in QuteBase\n        return altNames;\n    }\n\n    /**\n     * Pf2eTools ritual casting attributes\n     *\n     * This data object provides a default mechanism for creating\n     * a marked up string based on the attributes that are present.\n     * To use it, reference it directly: `{resource.casting}`.\n     */\n    @TemplateData\n    public static class QuteRitualCasting implements QuteUtil {\n        /**\n         * Duration to cast, as a {@link QuteDataDuration} which is either a {@link QuteDataActivity}, or a\n         * {@link QuteDataTimedDuration}.\n         */\n        public QuteDataDuration duration;\n        /** Formatted string. Material cost of the spell */\n        public String cost;\n        /** Minumum number of secondary casters required */\n        public String secondaryCasters;\n\n        public String toString() {\n            List<String> parts = new ArrayList<>();\n\n            if (duration != null) {\n                parts.add(\"**Cast** \" + duration);\n            }\n            if (isPresent(cost)) {\n                parts.add(String.format(\"**Cost** %s\", cost));\n            }\n            if (isPresent(secondaryCasters)) {\n                parts.add(String.format(\"**Secondary Casters** %s\", secondaryCasters));\n            }\n\n            return String.join(\"\\n- \", parts);\n        }\n    }\n\n    /**\n     * Pf2eTools ritual check attributes\n     *\n     * This data object provides a default mechanism for creating\n     * a marked up string based on the attributes that are present.\n     * To use it, reference it directly: `{resource.checks}`.\n     */\n    @TemplateData\n    public static class QuteRitualChecks implements QuteUtil {\n        /** Formatted string. Links to skills for primary checks */\n        public String primaryChecks;\n        /** Formatted string. Links to skills for secondary checks */\n        public String secondaryChecks;\n\n        public String toString() {\n            List<String> parts = new ArrayList<>();\n            parts.add(String.format(\"**Primary Checks** %s\", primaryChecks));\n\n            if (isPresent(secondaryChecks)) {\n                parts.add(String.format(\"**Secondary Checks** %s\", secondaryChecks));\n            }\n\n            return String.join(\"\\n- \", parts);\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteSpell.java",
    "content": "package dev.ebullient.convert.tools.pf2e.qute;\n\nimport static dev.ebullient.convert.StringUtil.join;\nimport static dev.ebullient.convert.StringUtil.joinConjunct;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\n\nimport dev.ebullient.convert.io.JavadocVerbatim;\nimport dev.ebullient.convert.qute.NamedText;\nimport dev.ebullient.convert.qute.QuteUtil;\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.Pf2eSources;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * Pf2eTools Spell attributes ({@code spell2md.txt})\n *\n * Extension of {@link dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase Pf2eQuteBase}\n */\n@TemplateData\npublic class QuteSpell extends Pf2eQuteBase {\n\n    private final List<String> altNames;\n\n    /** A spell’s overall power, from 1 to 10. */\n    public final String level;\n    /** Type: spell, cantrip, or focus */\n    public final String spellType;\n    /** Collection of traits (decorated links) */\n    public final Collection<String> traits;\n\n    /**\n     * The time it takes to cast the spell, as a {@link QuteDataDuration} which is either a {@link QuteDataActivity}\n     * or a {@link QuteDataTimedDuration}.\n     */\n    public final QuteDataDuration castDuration;\n    /**\n     * The required spell components as a list of formatted strings (maybe empty). Use\n     * {@link dev.ebullient.convert.tools.pf2e.qute.QuteSpell#formattedComponents()}\n     * to get a pre-formatted representation.\n     */\n    public final List<String> components;\n    /** The material cost of the spell as a formatted string (optional) */\n    public final String cost;\n    /** The activation trigger for the spell as a formatted string (optional) */\n    public final String trigger;\n    /** The requirements to cast the spell (optional) */\n    public final String requirements;\n    /** Spell target attributes as {@link dev.ebullient.convert.tools.pf2e.qute.QuteSpell.QuteSpellTarget QuteSpellTarget} */\n    public final QuteSpellTarget targeting;\n    /** Spell save, as {@link dev.ebullient.convert.tools.pf2e.qute.QuteSpell.QuteSpellSave} */\n    public final QuteSpellSave save;\n    /** Spell duration, as {@link QuteDataTimedDuration} */\n    public final QuteSpellDuration duration;\n    /** Psi amp behavior as {@link dev.ebullient.convert.tools.pf2e.qute.QuteSpell.QuteSpellAmp QuteSpellAmp} */\n    public final QuteSpellAmp amp;\n    /** List of spell domains (links) */\n    public final List<String> domains;\n    /** List of spell traditions (trait links) */\n    public final List<String> traditions;\n    /** Spell lists containing this spell */\n    public final List<String> spellLists;\n\n    /**\n     * List of category (Bloodline or Mystery) to Subclass (Sorcerer or Oracle). Link to class (if present)\n     * as a list of {@link dev.ebullient.convert.qute.NamedText NamedText}.\n     */\n    public final Collection<NamedText> subclass;\n    /** Heightened spell effects as a list of {@link dev.ebullient.convert.qute.NamedText NamedText} */\n    public final Collection<NamedText> heightened;\n\n    public QuteSpell(Pf2eSources sources, List<String> text, Tags tags,\n            String level, String spellType, Collection<String> traits, List<String> aliases,\n            QuteDataDuration castDuration, List<String> components, String cost, String trigger, String requirements,\n            QuteSpellTarget targeting, QuteSpellSave save, QuteSpellDuration duration,\n            List<String> domains, List<String> traditions, List<String> spellLists,\n            Collection<NamedText> subclass, Collection<NamedText> heightened, QuteSpellAmp amp) {\n        super(sources, text, tags);\n\n        this.level = level;\n        this.spellType = spellType;\n        this.traits = traits;\n        this.altNames = aliases;\n        this.castDuration = castDuration;\n        this.components = components;\n        this.cost = cost;\n        this.trigger = trigger;\n        this.requirements = requirements;\n        this.targeting = targeting;\n        this.save = save;\n        this.duration = duration;\n        this.domains = domains;\n        this.traditions = traditions;\n        this.spellLists = spellLists;\n        this.subclass = subclass;\n        this.heightened = heightened;\n        this.amp = amp;\n    }\n\n    @Override\n    public List<String> getAltNames() {\n        // Used by getAliases in QuteBase\n        return altNames;\n    }\n\n    /**\n     * True if the spell text has sections or Amp text\n     */\n    @Override\n    public boolean getHasSections() {\n        return super.getHasSections() || amp != null;\n    }\n\n    /**\n     * The components required for the spell, as a formatted string. Example:\n     *\n     * ```md\n     * [somatic](#), [verbal](#)\n     * ```\n     */\n    @JavadocVerbatim\n    public String formattedComponents() {\n        return join(\", \", components);\n    }\n\n    /**\n     * Details about the saving throw for a spell.\n     *\n     * Example default representations:\n     *\n     * - `basic Reflex or Fortitude`\n     * - `basic Reflex, Fortitude, or Willpower`\n     *\n     * @param saves The saving throws that can be used for this spell (list of strings)\n     * @param basic True if this is a basic save (boolean)\n     * @param hidden Whether this save should be hidden. This is sometimes true when it's a special save that is\n     *        described in the text of the spell.\n     */\n    @TemplateData\n    public record QuteSpellSave(List<String> saves, boolean basic, boolean hidden) implements QuteUtil {\n\n        @Override\n        public String toString() {\n            return join(\" \", basic ? \"basic\" : \"\", joinConjunct(\" or \", saves));\n        }\n    }\n\n    /**\n     * Details about the duration of the spell.\n     *\n     * Example default representations:\n     *\n     * - `1 minute`\n     * - `sustained up to 1 minute`\n     *\n     * @param sustained Whether this is a sustained spell, boolean\n     * @param dismissable Whether this spell can be dismissed, boolean. Not included in the default representation.\n     * @param duration The duration of this spell, as a {@link QuteDataTimedDuration}.\n     */\n    @TemplateData\n    public record QuteSpellDuration(\n            QuteDataTimedDuration duration, boolean sustained, boolean dismissable) implements QuteUtil {\n        @Override\n        public String toString() {\n            if (duration != null && duration.hasCustomDisplay()) {\n                return duration.toString();\n            }\n            if (sustained && (duration == null || duration.unit() == QuteDataTimedDuration.DurationUnit.UNLIMITED)) {\n                return \"sustained\";\n            }\n            if (duration == null) {\n                return \"\";\n            }\n            return (sustained ? \"sustained up to \" : \"\") + duration;\n        }\n    }\n\n    /**\n     * Pf2eTools spell target attributes.\n     *\n     * This attribute will render itself as labeled elements\n     * if you reference it directly: `{resource.targeting}`.\n     */\n    @TemplateData\n    public static class QuteSpellTarget implements QuteUtil {\n        /** The spell's range, as a {@link QuteDataRange}. */\n        public QuteDataRange range;\n        /** Formatted string describing the spell area of effect */\n        public String area;\n        /** Formatted string describing the spell target(s) */\n        public String targets;\n\n        public String toString() {\n            List<String> parts = new ArrayList<>();\n            if (isPresent(range)) {\n                parts.add(\"**Range**: \" + range);\n            }\n            if (isPresent(area)) {\n                parts.add(\"**Area**: \" + area);\n            }\n            if (isPresent(targets)) {\n                parts.add(\"**Targets**: \" + targets);\n            }\n            return String.join(\"\\n- \", parts);\n        }\n    }\n\n    /**\n     * Pf2eTools spell Amp attributes\n     *\n     * This attribute will render itself as labeled elements\n     * if you reference it directly: `{resource.amp}`.\n     */\n    @TemplateData\n    public static class QuteSpellAmp implements QuteUtil {\n        /** Formatted text describing amp effects */\n        public String text;\n        /** Heightened amp effects as a list of {@link dev.ebullient.convert.qute.NamedText Traits} */\n        public Collection<NamedText> ampEffects;\n\n        public String toString() {\n            List<String> parts = new ArrayList<>();\n            if (isPresent(text)) {\n                parts.add(text);\n            }\n            if (isPresent(ampEffects)) {\n                for (NamedText entry : ampEffects) {\n                    parts.add(\"**\" + entry.name + \"** \" + entry.desc);\n                }\n            }\n\n            return String.join(\"\\n\\n\", parts);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteTrait.java",
    "content": "package dev.ebullient.convert.tools.pf2e.qute;\n\nimport java.util.List;\n\nimport dev.ebullient.convert.tools.Tags;\nimport dev.ebullient.convert.tools.pf2e.Pf2eSources;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * Pf2eTools Trait attributes ({@code trait2md.txt})\n *\n * Extension of {@link dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteBase Pf2eQuteBase}\n */\n@TemplateData\npublic class QuteTrait extends Pf2eQuteBase {\n\n    private final List<String> altNames;\n\n    /** List of categories to which this trait belongs */\n    public final List<String> categories;\n\n    public QuteTrait(Pf2eSources sources, List<String> text, Tags tags,\n            List<String> aliases, List<String> categories) {\n        super(sources, text, tags);\n\n        this.altNames = aliases;\n        this.categories = categories;\n    }\n\n    @Override\n    public List<String> getAltNames() {\n        // Used by getAliases in QuteBase\n        return altNames;\n    }\n\n    @Override\n    public boolean createIndex() {\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/qute/QuteTraitIndex.java",
    "content": "package dev.ebullient.convert.tools.pf2e.qute;\n\nimport static dev.ebullient.convert.StringUtil.toAnchorTag;\n\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.TreeMap;\nimport java.util.stream.Collectors;\n\nimport dev.ebullient.convert.tools.pf2e.Pf2eIndexType;\nimport dev.ebullient.convert.tools.pf2e.Pf2eSources;\nimport io.quarkus.qute.TemplateData;\n\n/**\n * Pf2eTools Trait index attributes ({@code indexTrait.md})\n *\n * This replaces the index usually generated for folders.\n * The default template for the trait consructs a list of links to\n * traits grouped by category.\n *\n * Extension of {@link dev.ebullient.convert.tools.pf2e.qute.Pf2eQuteNote Pf2eQuteNote}\n */\n@TemplateData\npublic class QuteTraitIndex extends Pf2eQuteNote {\n\n    /** Map of category to a list of traits */\n    public final Map<String, Collection<String>> categoryToTraits;\n\n    public QuteTraitIndex(Pf2eSources sources, Map<String, Collection<String>> categoryToTraits) {\n        super(Pf2eIndexType.syntheticGroup, sources, \"Trait Index\");\n        this.categoryToTraits = new TreeMap<>();\n        for (Map.Entry<String, Collection<String>> entry : categoryToTraits.entrySet()) {\n            List<String> sorted = entry.getValue().stream()\n                    .filter(x -> x.matches(\"\\\\[.+?\\\\]\\\\(.+?\\\\)\"))\n                    .sorted()\n                    .collect(Collectors.toList());\n            if (!sorted.isEmpty()) {\n                this.categoryToTraits.put(entry.getKey(), sorted);\n            }\n        }\n    }\n\n    /** List of category anchor links */\n    public List<String> getCategoryLinks() {\n        return categoryToTraits.keySet().stream()\n                .map(x -> \"[\" + x + \"](#\" + toAnchorTag(x) + \")\")\n                .toList();\n    }\n\n    @Override\n    public String targetFile() {\n        return \"traits\";\n    }\n\n    @Override\n    public String targetPath() {\n        return Pf2eIndexType.trait.relativePath();\n    }\n\n    @Override\n    public String template() {\n        return \"indexTrait.txt\";\n    }\n}\n"
  },
  {
    "path": "src/main/java/dev/ebullient/convert/tools/pf2e/qute/package-info.java",
    "content": "/**\n * <h1>Pf2eTools templates</h1>\n *\n * Qute templates for generating content from Pf2eTools data.\n *\n * Pathfinder data uses a lot of inline and nested embedding,\n * which creates additional template variants and some special\n * behavior.\n */\npackage dev.ebullient.convert.tools.pf2e.qute;\n"
  },
  {
    "path": "src/main/resources/application.properties",
    "content": "quarkus.log.level=WARN\n\nquarkus.banner.enabled=false\nquarkus.test.continuous-testing=enabled\n\nquarkus.native.resources.includes=*.json,*.yaml,*.svg,*.properties,*.txt\nquarkus.native.additional-build-args=--enable-url-protocols=https,-H:Log=registerResource:3\n\n"
  },
  {
    "path": "src/main/resources/convertData.json",
    "content": "{\n  \"config5e\": {\n    \"constants\": {\n      \"imgRoot\": \"https://raw.githubusercontent.com/5etools-mirror-3/5etools-img/main/\",\n      \"tools5eSource\": \"https://api.github.com/repos/5etools-mirror-3/5etools-src/releases/latest\"\n    },\n    \"templateKeys\": [\n      \"background\",\n      \"bastion\",\n      \"class\",\n      \"deck\",\n      \"deity\",\n      \"feat\",\n      \"hazard\",\n      \"index\",\n      \"item\",\n      \"monster\",\n      \"note\",\n      \"object\",\n      \"psionic\",\n      \"race\",\n      \"reward\",\n      \"spell\",\n      \"subclass\",\n      \"vehicle\"\n    ],\n    \"srdEntries\": {\n      \"properties\": [\n        {\n          \"type\": \"entries\",\n          \"name\": \"Attunement\",\n          \"edition\": \"classic\",\n          \"source\": \"DMG\",\n          \"page\": 136,\n          \"basicRules\": true,\n          \"srd\": true,\n          \"reprintedAs\": [\n            {\n              \"tag\": \"reference\",\n              \"uid\": \"Attunement|XPHB\"\n            }\n          ],\n          \"entries\": [\n            \"Some magic items require a creature to form a bond with them before their magical properties can be used. This bond is called attunement, and certain items have a prerequisite for it. If the prerequisite is a class, a creature must be a member of that class to attune to the item. (If the class is a spellcasting class, a monster qualifies if it has spell slots and uses that class's spell list.) If the prerequisite is to be a spellcaster, a creature qualifies if it can cast at least one spell using its traits or features, not using a magic item or the like.\",\n            \"Without becoming attuned to an item that requires attunement, a creature gains only its nonmagical benefits, unless its description states otherwise. For example, a magic shield that requires attunement provides the benefits of a normal shield to a creature not attuned to it, but none of its magical properties.\",\n            \"Attuning to an item requires a creature to spend a short rest focused on only that item while being in physical contact with it (this can't be the same short rest used to learn the item's properties). This focus can take the form of weapon practice (for a weapon), meditation (for a wondrous item), or some other appropriate activity. If the short rest is interrupted, the attunement attempt fails. Otherwise, at the end of the short rest, the creature gains an intuitive understanding of how to activate any magical properties of the item, including any necessary command words.\",\n            \"An item can be attuned to only one creature at a time, and a creature can be attuned to no more than three magic items at a time. Any attempt to attune to a fourth item fails; the creature must end its attunement to an item first. Additionally, a creature can't attune to more than one copy of an item. For example, a creature can't attune to more than one ring of protection at a time.\",\n            \"A creature's attunement to an item ends if the creature no longer satisfies the prerequisites for attunement, if the item has been more than 100 feet away for at least 24 hours, if the creature dies, or if another creature attunes to the item. A creature can also voluntarily end attunement by spending another short rest focused on the item, unless the item is cursed.\",\n            {\n              \"type\": \"entries\",\n              \"name\": \"Optional Attunement\",\n              \"entries\": [\n                \"Attunement may be required for this item.\"\n              ]\n            },\n            {\n              \"type\": \"entries\",\n              \"name\": \"Requires Attunement\",\n              \"entries\": [\n                \"Attunement is required for this item.\"\n              ]\n            }\n          ]\n        },\n        {\n          \"type\": \"entries\",\n          \"name\": \"Attunement\",\n          \"edition\": \"one\",\n          \"source\": \"XPHB\",\n          \"page\": 232,\n          \"id\": \"717\",\n          \"basicRules2024\": true,\n          \"srd52\": true,\n          \"entries\": [\n            \"Some magic items require a creature to form a bond\\u2014called Attunement\\u2014with them before the creature can use an item's magical properties. Without becoming attuned to an item that requires Attunement, you gain only its nonmagical benefits unless its description states otherwise. For example, a magic Shield that requires Attunement provides the benefits of a normal Shield if you aren't attuned to it, but none of its magical properties.\",\n            {\n              \"type\": \"entries\",\n              \"name\": \"Attune during a Short Rest\",\n              \"page\": 232,\n              \"id\": \"718\",\n              \"entries\": [\n                \"Attuning to an item requires you to spend a {@variantrule Short Rest|XPHB} focused on only that item while being in physical contact with it (this can't be the same Short Rest used to learn the item's properties). This focus can take the form of weapon practice (for a Weapon), meditation (for a Wand), or some other appropriate activity. If the Short Rest is interrupted, the Attunement attempt fails. Otherwise, at the end of the Short Rest, you're attuned to the magic item and can access its full magical capabilities.\"\n              ]\n            },\n            {\n              \"type\": \"entries\",\n              \"name\": \"No More Than Three Items\",\n              \"page\": 232,\n              \"id\": \"719\",\n              \"entries\": [\n                \"You can be attuned to no more than three magic items at a time. Any attempt to attune to a fourth item fails; you must end your Attunement to an item first. Additionally, you can't attune to more than one copy of an item. For example, you can't attune to more than one {@item Ring of Protection} at a time.\"\n              ]\n            },\n            {\n              \"type\": \"entries\",\n              \"name\": \"Ending Attunement\",\n              \"page\": 232,\n              \"id\": \"71a\",\n              \"entries\": [\n                \"Your Attunement to an item ends if you no longer satisfy the prerequisites for Attunement, if the item has been more than 100 feet away for at least 24 hours, if you die, or if another creature attunes to the item. You can also voluntarily end Attunement by spending another {@variantrule Short Rest|XPHB} focused on the item unless the item is cursed.\"\n              ]\n            },\n            {\n              \"type\": \"entries\",\n              \"name\": \"Optional Attunement\",\n              \"entries\": [\n                \"Attunement may be required for this item.\"\n              ]\n            },\n            {\n              \"type\": \"entries\",\n              \"name\": \"Requires Attunement\",\n              \"entries\": [\n                \"Attunement is required for this item.\"\n              ]\n            }\n          ]\n        },\n        {\n          \"name\": \"General and Weapon Properties\",\n          \"srd\": true,\n          \"srd52\": true,\n          \"basicRules\": true,\n          \"basicRules2024\": true,\n          \"entries\": [\n            \"Many weapons have special properties related to their use, as shown in the Weapons table.\"\n          ]\n        },\n        {\n          \"name\": \"Improvised Weapons\",\n          \"edition\": \"classic\",\n          \"source\": \"PHB\",\n          \"page\": 147,\n          \"srd\": true,\n          \"basicRules\": true,\n          \"reprintedAs\": [\n            {\n              \"tag\": \"reference\",\n              \"uid\": \"Improvised Weapons|XPHB\"\n            }\n          ],\n          \"entries\": [\n            \"Sometimes characters don't have their weapons and have to attack with whatever is close at hand. An improvised weapon includes any object you can wield in one or two hands, such as broken glass, a table leg, a frying pan, a wagon wheel, or a dead goblin.\",\n            \"In many cases, an improvised weapon is similar to an actual weapon and can be treated as such. For example, a table leg is akin to a club. At the DM's option, a character proficient with a weapon can use a similar object as if it were that weapon and use his or her proficiency bonus.\",\n            \"An object that bears no resemblance to a weapon deals 1d4 damage (the DM assigns a damage type appropriate to the object). If a character uses a ranged weapon to make a melee attack, or throws a melee weapon that does not have the thrown property, it also deals 1d4 damage. An improvised thrown weapon has a normal range of 20 feet and a long range of 60 feet.\"\n          ]\n        },\n        {\n          \"name\": \"Improvised Weapons\",\n          \"edition\": \"one\",\n          \"source\": \"XPHB\",\n          \"srd52\": true,\n          \"basicRules2024\": true,\n          \"page\": 213,\n          \"id\": \"645\",\n          \"entries\": [\n            \"If you use an object\\u2014such as a table leg, frying pan, or bottle\\u2014as a makeshift weapon, see \\\"{@variantrule Improvised Weapons|XPHB}\\\" in the {@book rules glossary|XPHB|10|Improvised Weapons}. Also see those rules if you wield a weapon in an unusual way, such as using a Ranged weapon to make a melee attack.\"\n          ]\n        },\n        {\n          \"name\": \"Silvered Weapons\",\n          \"srd\": true,\n          \"srd52\": true,\n          \"basicRules\": true,\n          \"basicRules2024\": true,\n          \"entries\": [\n            \"Some monsters that have immunity or resistance to nonmagical weapons are susceptible to silver weapons, so cautious adventurers invest extra coin to plate their weapons with silver. You can silver a single weapon or ten pieces of ammunition for 100 gp. This cost represents not only the price of the silver, but the time and expertise needed to add silver to the weapon without making it less effective.\"\n          ]\n        },\n        {\n          \"name\": \"Special Weapons\",\n          \"srd\": true,\n          \"basicRules\": true,\n          \"source\": \"PHB\",\n          \"page\": 148,\n          \"entries\": [\n            \"Weapons with special rules are described here.\",\n            {\n              \"type\": \"entries\",\n              \"name\": \"Lance\",\n              \"entries\": [\n                \"You have disadvantage when you use a lance to attack a target within 5 feet of you. Also, a lance requires two hands to wield when you aren't mounted.\"\n              ]\n            },\n            {\n              \"type\": \"entries\",\n              \"name\": \"Net\",\n              \"entries\": [\n                \"A Large or smaller creature hit by a net is {@condition restrained} until it is freed. A net has no effect on creatures that are formless, or creatures that are Huge or larger. A creature can use its action to make a DC 10 Strength check, freeing itself or another creature within its reach on a success. Dealing 5 slashing damage to the net (AC 10) also frees the creature without harming it, ending the effect and destroying the net.\",\n                \"When you use an action, bonus action, or reaction to attack with a net, you can make only one attack regardless of the number of attacks you can normally make.\"\n              ]\n            }\n          ]\n        },\n        {\n          \"type\": \"entries\",\n          \"name\": \"Cursed Items\",\n          \"source\": \"DMG\",\n          \"page\": 138,\n          \"basicRules\": true,\n          \"reprintedAs\": [\n            {\n              \"tag\": \"reference\",\n              \"uid\": \"Cursed Items|XDMG\"\n            }\n          ],\n          \"entries\": [\n            \"Some magic items bear curses that bedevil their users, sometimes long after a user has stopped using an item. Most methods of identifying items, including the identify spell, fail to reveal the presence of a curse, although lore might hint at it.\",\n            \"Attunement to a cursed item can't be ended voluntarily unless the curse is broken first, such as with the {@spell remove curse} spell.\"\n          ]\n        },\n        {\n          \"type\": \"entries\",\n          \"name\": \"Cursed Items\",\n          \"source\": \"XDMG\",\n          \"page\": 220,\n          \"basicRules2024\": true,\n          \"entries\": [\n            \"A magic item’s description specifies whether it bears a curse. Most methods of identifying items, including the Identify spell, fail to reveal such a curse.\",\n            \"Attunement to a cursed item can't be ended voluntarily unless the curse is broken first, such as with the {@spell Remove Curse} spell.\"\n          ]\n        },\n        {\n          \"type\": \"entries\",\n          \"name\": \"Poison\",\n          \"source\": \"DMG\",\n          \"page\": 257,\n          \"srd\": true,\n          \"basicRules\": true,\n          \"reprintedAs\": [\n            {\n              \"tag\": \"reference\",\n              \"uid\": \"Poison|XDMG\"\n            }\n          ],\n          \"entries\": [\n            \"Given their insidious and deadly nature, poisons are illegal in most societies but are a favorite tool among assassins, drow, and other evil creatures.\",\n            \"Poisons come in the following four types.\",\n            {\n              \"type\": \"entries\",\n              \"name\": \"Contact\",\n              \"page\": 257,\n              \"entries\": [\n                \"Contact poison can be smeared on an object and remains potent until it is touched or washed off. A creature that touches contact poison with exposed skin suffers its effects.\"\n              ],\n              \"id\": \"2f9\"\n            },\n            {\n              \"type\": \"entries\",\n              \"name\": \"Ingested\",\n              \"page\": 257,\n              \"entries\": [\n                \"A creature must swallow an entire dose of ingested poison to suffer its effects. You might decide that a partial dose has a reduced effect, such as allowing advantage on the saving throw or dealing only half damage on a failed save. The dose can be delivered in food or a liquid.\"\n              ],\n              \"id\": \"2fa\"\n            },\n            {\n              \"type\": \"entries\",\n              \"name\": \"Inhaled\",\n              \"page\": 257,\n              \"entries\": [\n                \"These poisons are powders or gases that take effect when inhaled. Blowing the powder or releasing the gas subjects creatures in a 5-foot cube to its effect. The resulting cloud dissipates immediately afterward. Holding one's breath is ineffective against inhaled poisons, as they affect nasal membranes, tear ducts, and other parts of the body.\"\n              ],\n              \"id\": \"2fb\"\n            },\n            {\n              \"type\": \"entries\",\n              \"name\": \"Injury\",\n              \"page\": 257,\n              \"entries\": [\n                \"Injury poison can be applied to weapons, ammunition, trap components, and other objects that deal piercing or slashing damage and remains potent until delivered through a wound or washed off. A creature that takes piercing or slashing damage from an object coated with the poison is exposed to its effects.\"\n              ],\n              \"id\": \"2fc\"\n            }\n          ]\n        },\n        {\n          \"name\": \"Poison\",\n          \"source\": \"XDMG\",\n          \"srd52\": true,\n          \"basicRules2024\": true,\n          \"page\": 90,\n          \"entries\": [\n            \"Given their insidious and deadly nature, poisons are a favorite tool among assassins and evil creatures.\",\n            \"Poisons come in the following four types:\",\n            {\n              \"type\": \"list\",\n              \"style\": \"list-hang-notitle\",\n              \"items\": [\n                {\n                  \"type\": \"item\",\n                  \"name\": \"Contact\",\n                  \"entry\": \"Contact poison can be smeared on an object and remains potent until it is touched or washed off. A creature that touches contact poison with exposed skin suffers its effects.\"\n                },\n                {\n                  \"type\": \"item\",\n                  \"name\": \"Ingested\",\n                  \"entry\": \"A creature must swallow an entire dose of ingested poison to suffer its effects. The dose can be delivered in food or a liquid. You may decide that a partial dose has a reduced effect, such as allowing {@variantrule Advantage|XPHB} on the saving throw or dealing only half as much damage on a failed save.\"\n                },\n                {\n                  \"type\": \"item\",\n                  \"name\": \"Inhaled\",\n                  \"entry\": \"Poisonous powders and gases take effect when inhaled. Blowing the powder or releasing the gas subjects creatures in a 5-foot {@variantrule Cube [Area of Effect]|XPHB|Cube} to its effect. The resulting cloud dissipates immediately afterward. Holding one's breath is ineffective against inhaled poisons, as they affect nasal membranes, tear ducts, and other parts of the body.\"\n                },\n                {\n                  \"type\": \"item\",\n                  \"name\": \"Injury\",\n                  \"entry\": \"Injury poison can be applied as a Bonus Action to a weapon, a piece of ammunition, or similar object. The poison remains potent until delivered through a wound or washed off. A creature that takes Piercing or Slashing damage from an object coated with the poison is exposed to its effects.\"\n                }\n              ]\n            },\n            {\n              \"type\": \"entries\",\n              \"name\": \"Purchasing Poison\",\n              \"page\": 90,\n              \"id\": \"1be\",\n              \"entries\": [\n                \"In some settings, laws prohibit the possession and use of poison, but an illicit dealer or unscrupulous apothecary might keep a hidden stash. Characters with criminal contacts might be able to acquire poison easily. Other characters might have to make extensive inquiries and pay bribes before they acquire the poison they seek.\"\n              ]\n            },\n            {\n              \"type\": \"entries\",\n              \"name\": \"Harvesting Poison\",\n              \"page\": 90,\n              \"id\": \"1bf\",\n              \"entries\": [\n                \"A character can attempt to harvest poison from a venomous creature that is dead or has the {@condition Incapacitated|XPHB} condition. The effort takes {@dice 1d6} minutes, after which the character makes a {@dc 20} Intelligence ({@skill Nature|XPHB}) check using a {@item Poisoner's Kit|XPHB}. On a successful check, the character harvests enough poison for a single dose, and no additional poison can be harvested from that creature. On a failed check, the character is unable to extract any poison. If the character fails the check by 5 or more, the character is subjected to the creature's poison.\"\n              ]\n            }\n          ]\n        }\n      ]\n    },\n    \"basicRules\": [\n      \"action|attack|phb\",\n      \"action|cast a spell|phb\",\n      \"action|dash|phb\",\n      \"action|disengage|phb\",\n      \"action|dodge|phb\",\n      \"action|escape a grapple|phb\",\n      \"action|grapple|phb\",\n      \"action|help|phb\",\n      \"action|hide|phb\",\n      \"action|opportunity attack|phb\",\n      \"action|ready|phb\",\n      \"action|search|phb\",\n      \"action|shove|phb\",\n      \"action|two-weapon fighting|phb\",\n      \"action|use an object|phb\",\n      \"background|acolyte|phb\",\n      \"background|criminal|phb\",\n      \"background|folk hero|phb\",\n      \"background|noble|phb\",\n      \"background|sage|phb\",\n      \"background|soldier|phb\",\n      \"classtype|cleric|phb\",\n      \"classtype|fighter|phb\",\n      \"classtype|rogue|phb\",\n      \"classtype|wizard|phb\",\n      \"condition|blinded|phb\",\n      \"condition|charmed|phb\",\n      \"condition|deafened|phb\",\n      \"condition|frightened|phb\",\n      \"condition|grappled|phb\",\n      \"condition|incapacitated|phb\",\n      \"condition|invisible|phb\",\n      \"condition|paralyzed|phb\",\n      \"condition|petrified|phb\",\n      \"condition|poisoned|phb\",\n      \"condition|prone|phb\",\n      \"condition|restrained|phb\",\n      \"condition|stunned|phb\",\n      \"condition|unconscious|phb\",\n      \"itemproperty|2h|phb\",\n      \"itemproperty|a|phb\",\n      \"itemproperty|f|phb\",\n      \"itemproperty|h|phb\",\n      \"itemproperty|ld|phb\",\n      \"itemproperty|l|phb\",\n      \"itemproperty|rld|dmg\",\n      \"itemproperty|r|phb\",\n      \"itemproperty|s|phb\",\n      \"itemproperty|t|phb\",\n      \"itemproperty|v|phb\",\n      \"itemtype|$c|phb\",\n      \"itemtype|at|phb\",\n      \"itemtype|a|phb\",\n      \"itemtype|fd|phb\",\n      \"itemtype|gs|phb\",\n      \"itemtype|g|phb\",\n      \"itemtype|ha|phb\",\n      \"itemtype|ins|phb\",\n      \"itemtype|la|phb\",\n      \"itemtype|ma|phb\",\n      \"itemtype|mnt|phb\",\n      \"itemtype|m|phb\",\n      \"itemtype|oth|phb\",\n      \"itemtype|p|phb\",\n      \"itemtype|r|phb\",\n      \"itemtype|scf|phb\",\n      \"itemtype|s|phb\",\n      \"itemtype|tah|phb\",\n      \"itemtype|tg|phb\",\n      \"itemtype|t|phb\",\n      \"itemtype|veh|phb\",\n      \"race|dwarf|phb\",\n      \"race|elf|phb\",\n      \"race|halfling|phb\",\n      \"race|human|phb\",\n      \"sense|blindsight|phb\",\n      \"sense|darkvision|phb\",\n      \"sense|truesight|phb\",\n      \"skill|acrobatics|phb\",\n      \"skill|animal handling|phb\",\n      \"skill|arcana|phb\",\n      \"skill|athletics|phb\",\n      \"skill|deception|phb\",\n      \"skill|history|phb\",\n      \"skill|insight|phb\",\n      \"skill|intimidation|phb\",\n      \"skill|investigation|phb\",\n      \"skill|medicine|phb\",\n      \"skill|nature|phb\",\n      \"skill|perception|phb\",\n      \"skill|performance|phb\",\n      \"skill|persuasion|phb\",\n      \"skill|religion|phb\",\n      \"skill|sleight of hand|phb\",\n      \"skill|stealth|phb\",\n      \"skill|survival|phb\",\n      \"subclass|champion|fighter|phb|phb\",\n      \"subclass|life domain|cleric|phb|phb\",\n      \"subclass|school of evocation|wizard|phb|phb\",\n      \"subclass|thief|rogue|phb|phb\",\n      \"subrace|dwarf (hill)|dwarf|phb|phb\",\n      \"subrace|dwarf (mountain)|dwarf|phb|phb\",\n      \"subrace|elf (high)|elf|phb|phb\",\n      \"subrace|elf (wood)|elf|phb|phb\",\n      \"subrace|halfling (lightfoot)|halfling|phb|phb\",\n      \"subrace|halfling (stout)|halfling|phb|phb\",\n      \"subrace|human (variant)|human|phb|phb\"\n    ],\n    \"basicRules2024\": [\n      \"action|attack|xphb\",\n      \"action|dash|xphb\",\n      \"action|disengage|xphb\",\n      \"action|dodge|xphb\",\n      \"action|escape a grapple|xphb\",\n      \"action|help|xphb\",\n      \"action|hide|xphb\",\n      \"action|influence|xphb\",\n      \"action|magic|xphb\",\n      \"action|opportunity attack|xphb\",\n      \"action|ready|xphb\",\n      \"action|search|xphb\",\n      \"action|study|xphb\",\n      \"action|two-weapon fighting|xphb\",\n      \"action|utilize|xphb\",\n      \"background|acolyte|xphb\",\n      \"background|criminal|xphb\",\n      \"background|sage|xphb\",\n      \"background|soldier|xphb\",\n      \"classtype|barbarian|xphb\",\n      \"classtype|bard|xphb\",\n      \"classtype|cleric|xphb\",\n      \"classtype|druid|xphb\",\n      \"classtype|fighter|xphb\",\n      \"classtype|monk|xphb\",\n      \"classtype|paladin|xphb\",\n      \"classtype|ranger|xphb\",\n      \"classtype|rogue|xphb\",\n      \"classtype|sorcerer|xphb\",\n      \"classtype|warlock|xphb\",\n      \"classtype|wizard|xphb\",\n      \"condition|blinded|xphb\",\n      \"condition|charmed|xphb\",\n      \"condition|deafened|xphb\",\n      \"condition|exhaustion|xphb\",\n      \"condition|frightened|xphb\",\n      \"condition|grappled|xphb\",\n      \"condition|incapacitated|xphb\",\n      \"condition|invisible|xphb\",\n      \"condition|paralyzed|xphb\",\n      \"condition|petrified|xphb\",\n      \"condition|poisoned|xphb\",\n      \"condition|prone|xphb\",\n      \"condition|restrained|xphb\",\n      \"condition|stunned|xphb\",\n      \"condition|unconscious|xphb\",\n      \"hazard|burning|xphb\",\n      \"hazard|dehydration|xphb\",\n      \"hazard|falling|xphb\",\n      \"hazard|malnutrition|xphb\",\n      \"hazard|suffocation|xphb\",\n      \"feat|ability score improvement|xphb\",\n      \"feat|alert|xphb\",\n      \"feat|archery|xphb\",\n      \"feat|boon of combat prowess|xphb\",\n      \"feat|boon of dimensional travel|xphb\",\n      \"feat|boon of fate|xphb\",\n      \"feat|boon of irresistible offense|xphb\",\n      \"feat|boon of the night spirit|xphb\",\n      \"feat|boon of truesight|xphb\",\n      \"feat|defense|xphb\",\n      \"feat|great weapon fighting|xphb\",\n      \"feat|magic initiate|xphb\",\n      \"feat|savage attacker|xphb\",\n      \"feat|skilled|xphb\",\n      \"feat|two-weapon fighting|xphb\",\n      \"itemproperty|2h|xphb\",\n      \"itemproperty|a|xphb\",\n      \"itemproperty|f|xphb\",\n      \"itemproperty|h|xphb\",\n      \"itemproperty|ld|xphb\",\n      \"itemproperty|l|xphb\",\n      \"itemproperty|rld|xdmg\",\n      \"itemproperty|r|xphb\",\n      \"itemproperty|t|xphb\",\n      \"itemproperty|v|xphb\",\n      \"itemtype|$c|xphb\",\n      \"itemtype|air|xphb\",\n      \"itemtype|at|xphb\",\n      \"itemtype|a|xphb\",\n      \"itemtype|fd|xphb\",\n      \"itemtype|gs|xphb\",\n      \"itemtype|g|xphb\",\n      \"itemtype|ha|xphb\",\n      \"itemtype|ins|xphb\",\n      \"itemtype|la|xphb\",\n      \"itemtype|ma|xphb\",\n      \"itemtype|mnt|xphb\",\n      \"itemtype|m|xphb\",\n      \"itemtype|p|xphb\",\n      \"itemtype|r|xphb\",\n      \"itemtype|scf|xphb\",\n      \"itemtype|sc|xphb\",\n      \"itemtype|shp|xphb\",\n      \"itemtype|s|xphb\",\n      \"itemtype|tah|xphb\",\n      \"itemtype|t|xphb\",\n      \"itemtype|veh|xphb\",\n      \"monster|allosaurus|xmm\",\n      \"monster|animated flying sword|xmm\",\n      \"monster|ankylosaurus|xmm\",\n      \"monster|ape|xmm\",\n      \"monster|archelon|xmm\",\n      \"monster|baboon|xmm\",\n      \"monster|badger|xmm\",\n      \"monster|bandit|xmm\",\n      \"monster|bat|xmm\",\n      \"monster|berserker|xmm\",\n      \"monster|black bear|xmm\",\n      \"monster|black dragon wyrmling|xmm\",\n      \"monster|blink dog|xmm\",\n      \"monster|blood hawk|xmm\",\n      \"monster|boar|xmm\",\n      \"monster|brown bear|xmm\",\n      \"monster|bugbear stalker|xmm\",\n      \"monster|bugbear warrior|xmm\",\n      \"monster|bullywug bog sage|xmm\",\n      \"monster|bullywug warrior|xmm\",\n      \"monster|camel|xmm\",\n      \"monster|carrion crawler|xmm\",\n      \"monster|cat|xmm\",\n      \"monster|commoner|xmm\",\n      \"monster|constrictor snake|xmm\",\n      \"monster|copper dragon wyrmling|xmm\",\n      \"monster|crab|xmm\",\n      \"monster|crocodile|xmm\",\n      \"monster|cultist|xmm\",\n      \"monster|cultist fanatic|xmm\",\n      \"monster|darkmantle|xmm\",\n      \"monster|deer|xmm\",\n      \"monster|dire wolf|xmm\",\n      \"monster|doppelganger|xmm\",\n      \"monster|draft horse|xmm\",\n      \"monster|eagle|xmm\",\n      \"monster|elephant|xmm\",\n      \"monster|elk|xmm\",\n      \"monster|fire elemental|xmm\",\n      \"monster|flying snake|xmm\",\n      \"monster|frog|xmm\",\n      \"monster|gelatinous cube|xmm\",\n      \"monster|giant badger|xmm\",\n      \"monster|giant centipede|xmm\",\n      \"monster|giant crab|xmm\",\n      \"monster|giant fire beetle|xmm\",\n      \"monster|giant goat|xmm\",\n      \"monster|giant seahorse|xmm\",\n      \"monster|giant spider|xmm\",\n      \"monster|giant weasel|xmm\",\n      \"monster|gnoll warrior|xmm\",\n      \"monster|goat|xmm\",\n      \"monster|goblin minion|xmm\",\n      \"monster|goblin warrior|xmm\",\n      \"monster|goblin boss|xmm\",\n      \"monster|gray ooze|xmm\",\n      \"monster|guard captain|xmm\",\n      \"monster|guard|xmm\",\n      \"monster|hawk|xmm\",\n      \"monster|hippopotamus|xmm\",\n      \"monster|hobgoblin captain|xmm\",\n      \"monster|hobgoblin warrior|xmm\",\n      \"monster|hunter shark|xmm\",\n      \"monster|hyena|xmm\",\n      \"monster|imp|xmm\",\n      \"monster|incubus|xmm\",\n      \"monster|jackal|xmm\",\n      \"monster|killer whale|xmm\",\n      \"monster|knight|xmm\",\n      \"monster|kobold warrior|xmm\",\n      \"monster|lion|xmm\",\n      \"monster|lizard|xmm\",\n      \"monster|mage|xmm\",\n      \"monster|mammoth|xmm\",\n      \"monster|mastiff|xmm\",\n      \"monster|mimic|xmm\",\n      \"monster|minotaur of baphomet|xmm\",\n      \"monster|mule|xmm\",\n      \"monster|noble|xmm\",\n      \"monster|nothic|xmm\",\n      \"monster|octopus|xmm\",\n      \"monster|ogre|xmm\",\n      \"monster|owl|xmm\",\n      \"monster|owlbear|xmm\",\n      \"monster|panther|xmm\",\n      \"monster|piranha|xmm\",\n      \"monster|pirate captain|xmm\",\n      \"monster|pirate|xmm\",\n      \"monster|plesiosaurus|xmm\",\n      \"monster|polar bear|xmm\",\n      \"monster|pony|xmm\",\n      \"monster|pseudodragon|xmm\",\n      \"monster|quasit|xmm\",\n      \"monster|rat|xmm\",\n      \"monster|raven|xmm\",\n      \"monster|red dragon wyrmling|xmm\",\n      \"monster|reef shark|xmm\",\n      \"monster|rhinoceros|xmm\",\n      \"monster|riding horse|xmm\",\n      \"monster|saber-toothed tiger|xmm\",\n      \"monster|scorpion|xmm\",\n      \"monster|scout|xmm\",\n      \"monster|seahorse|xmm\",\n      \"monster|shadow|xmm\",\n      \"monster|silver dragon wyrmling|xmm\",\n      \"monster|skeleton|xmm\",\n      \"monster|slaad tadpole|xmm\",\n      \"monster|sphinx of wonder|xmm\",\n      \"monster|spider|xmm\",\n      \"monster|sprite|xmm\",\n      \"monster|steam mephit|xmm\",\n      \"monster|stirge|xmm\",\n      \"monster|stone golem|xmm\",\n      \"monster|succubus|xmm\",\n      \"monster|swarm of bats|xmm\",\n      \"monster|swarm of crawling claws|xmm\",\n      \"monster|swarm of insects|xmm\",\n      \"monster|swarm of piranhas|xmm\",\n      \"monster|swarm of rats|xmm\",\n      \"monster|swarm of ravens|xmm\",\n      \"monster|swarm of venomous snakes|xmm\",\n      \"monster|tiger|xmm\",\n      \"monster|tough boss|xmm\",\n      \"monster|tough|xmm\",\n      \"monster|triceratops|xmm\",\n      \"monster|tyrannosaurus rex|xmm\",\n      \"monster|vampire familiar|xmm\",\n      \"monster|venomous snake|xmm\",\n      \"monster|vulture|xmm\",\n      \"monster|warhorse|xmm\",\n      \"monster|warrior infantry|xmm\",\n      \"monster|weasel|xmm\",\n      \"monster|wolf|xmm\",\n      \"monster|worg|xmm\",\n      \"monster|wraith|xmm\",\n      \"monster|zombie|xmm\",\n      \"monster|ogre zombie|xmm\",\n      \"optfeature|agonizing blast|xphb\",\n      \"optfeature|armor of shadows|xphb\",\n      \"optfeature|ascendant step|xphb\",\n      \"optfeature|devil's sight|xphb\",\n      \"optfeature|devouring blade|xphb\",\n      \"optfeature|eldritch mind|xphb\",\n      \"optfeature|eldritch smite|xphb\",\n      \"optfeature|eldritch spear|xphb\",\n      \"optfeature|fiendish vigor|xphb\",\n      \"optfeature|gaze of two minds|xphb\",\n      \"optfeature|gift of the depths|xphb\",\n      \"optfeature|gift of the protectors|xphb\",\n      \"optfeature|investment of the chain master|xphb\",\n      \"optfeature|lessons of the first ones|xphb\",\n      \"optfeature|lifedrinker|xphb\",\n      \"optfeature|mask of many faces|xphb\",\n      \"optfeature|master of myriad forms|xphb\",\n      \"optfeature|misty visions|xphb\",\n      \"optfeature|one with shadows|xphb\",\n      \"optfeature|otherworldly leap|xphb\",\n      \"optfeature|pact of the blade|xphb\",\n      \"optfeature|pact of the chain|xphb\",\n      \"optfeature|pact of the tome|xphb\",\n      \"optfeature|repelling blast|xphb\",\n      \"optfeature|thirsting blade|xphb\",\n      \"optfeature|visions of distant realms|xphb\",\n      \"optfeature|whispers of the grave|xphb\",\n      \"optfeature|witch sight|xphb\",\n      \"optfeature|careful spell|xphb\",\n      \"optfeature|distant spell|xphb\",\n      \"optfeature|empowered spell|xphb\",\n      \"optfeature|extended spell|xphb\",\n      \"optfeature|heightened spell|xphb\",\n      \"optfeature|quickened spell|xphb\",\n      \"optfeature|subtle spell|xphb\",\n      \"optfeature|transmuted spell|xphb\",\n      \"optfeature|twinned spell|xphb\",\n      \"race|dwarf|xphb\",\n      \"race|elf|xphb\",\n      \"race|halfling|xphb\",\n      \"race|human|xphb\",\n      \"sense|blindsight|xphb\",\n      \"sense|darkvision|xphb\",\n      \"sense|tremorsense|xphb\",\n      \"sense|truesight|xphb\",\n      \"skill|acrobatics|xphb\",\n      \"skill|animal handling|xphb\",\n      \"skill|arcana|xphb\",\n      \"skill|athletics|xphb\",\n      \"skill|deception|xphb\",\n      \"skill|history|xphb\",\n      \"skill|insight|xphb\",\n      \"skill|intimidation|xphb\",\n      \"skill|investigation|xphb\",\n      \"skill|medicine|xphb\",\n      \"skill|nature|xphb\",\n      \"skill|perception|xphb\",\n      \"skill|performance|xphb\",\n      \"skill|persuasion|xphb\",\n      \"skill|religion|xphb\",\n      \"skill|sleight of hand|xphb\",\n      \"skill|stealth|xphb\",\n      \"skill|survival|xphb\",\n      \"subclass|champion|fighter|xphb|xphb\",\n      \"subclass|circle of the land|druid|xphb|xphb\",\n      \"subclass|college of lore|bard|xphb|xphb\",\n      \"subclass|draconic sorcery|sorcerer|xphb|xphb\",\n      \"subclass|evoker|wizard|xphb|xphb\",\n      \"subclass|fiend patron|warlock|xphb|xphb\",\n      \"subclass|hunter|ranger|xphb|xphb\",\n      \"subclass|life domain|cleric|xphb|xphb\",\n      \"subclass|oath of devotion|paladin|xphb|xphb\",\n      \"subclass|path of the berserker|barbarian|xphb|xphb\",\n      \"subclass|thief|rogue|xphb|xphb\",\n      \"subclass|warrior of the open hand|monk|xphb|xphb\",\n      \"trap|collapsing roof|xdmg\",\n      \"trap|falling net|xdmg\",\n      \"trap|fire-casting statue|xdmg\",\n      \"trap|hidden pit|xdmg\",\n      \"trap|poisoned darts|xdmg\",\n      \"trap|poisoned needle|xdmg\",\n      \"trap|rolling stone|xdmg\",\n      \"trap|spiked pit|xdmg\",\n      \"variantrule|ability check|xphb\",\n      \"variantrule|ability score and modifier|xphb\",\n      \"variantrule|action|xphb\",\n      \"variantrule|advantage|xphb\",\n      \"variantrule|adventure|xphb\",\n      \"variantrule|alignment|xphb\",\n      \"variantrule|ally|xphb\",\n      \"variantrule|area of effect|xphb\",\n      \"variantrule|armor class|xphb\",\n      \"variantrule|armor training|xphb\",\n      \"variantrule|attack roll|xphb\",\n      \"variantrule|attitude|xphb\",\n      \"variantrule|attunement|xphb\",\n      \"variantrule|bloodied|xphb\",\n      \"variantrule|bonus action|xphb\",\n      \"variantrule|breaking objects|xphb\",\n      \"variantrule|bright light|xphb\",\n      \"variantrule|burrow speed|xphb\",\n      \"variantrule|campaign|xphb\",\n      \"variantrule|cantrip|xphb\",\n      \"variantrule|carrying capacity|xphb\",\n      \"variantrule|challenge rating|xphb\",\n      \"variantrule|character sheet|xphb\",\n      \"variantrule|climb speed|xphb\",\n      \"variantrule|climbing|xphb\",\n      \"variantrule|condition|xphb\",\n      \"variantrule|cone [area of effect]|xphb\",\n      \"variantrule|cover|xphb\",\n      \"variantrule|crawling|xphb\",\n      \"variantrule|creature type|xphb\",\n      \"variantrule|creature|xphb\",\n      \"variantrule|critical hit|xphb\",\n      \"variantrule|cube [area of effect]|xphb\",\n      \"variantrule|curses|xphb\",\n      \"variantrule|cylinder [area of effect]|xphb\",\n      \"variantrule|d20 test|xphb\",\n      \"variantrule|damage roll|xphb\",\n      \"variantrule|damage threshold|xphb\",\n      \"variantrule|damage types|xphb\",\n      \"variantrule|damage|xphb\",\n      \"variantrule|darkness|xphb\",\n      \"variantrule|dead|xphb\",\n      \"variantrule|death saving throw|xphb\",\n      \"variantrule|difficult terrain|xphb\",\n      \"variantrule|difficulty class|xphb\",\n      \"variantrule|dim light|xphb\",\n      \"variantrule|disadvantage|xphb\",\n      \"variantrule|encounter|xphb\",\n      \"variantrule|enemy|xphb\",\n      \"variantrule|experience points|xphb\",\n      \"variantrule|expertise|xphb\",\n      \"variantrule|fly speed|xphb\",\n      \"variantrule|flying|xphb\",\n      \"variantrule|friendly [attitude]|xphb\",\n      \"variantrule|grappling|xphb\",\n      \"variantrule|hazard|xphb\",\n      \"variantrule|healing|xphb\",\n      \"variantrule|heavily obscured|xphb\",\n      \"variantrule|heroic inspiration|xphb\",\n      \"variantrule|high jump|xphb\",\n      \"variantrule|hit point dice|xphb\",\n      \"variantrule|hit points|xphb\",\n      \"variantrule|hostile [attitude]|xphb\",\n      \"variantrule|hover|xphb\",\n      \"variantrule|illusions|xphb\",\n      \"variantrule|immunity|xphb\",\n      \"variantrule|improvised weapons|xphb\",\n      \"variantrule|indifferent [attitude]|xphb\",\n      \"variantrule|initiative|xphb\",\n      \"variantrule|jumping|xphb\",\n      \"variantrule|knocking out a creature|xphb\",\n      \"variantrule|lightly obscured|xphb\",\n      \"variantrule|long jump|xphb\",\n      \"variantrule|long rest|xphb\",\n      \"variantrule|magical effect|xphb\",\n      \"variantrule|monster|xphb\",\n      \"variantrule|nonplayer character|xphb\",\n      \"variantrule|object|xphb\",\n      \"variantrule|occupied space|xphb\",\n      \"variantrule|passive perception|xphb\",\n      \"variantrule|per day|xphb\",\n      \"variantrule|player character|xphb\",\n      \"variantrule|possession|xphb\",\n      \"variantrule|proficiency|xphb\",\n      \"variantrule|reaction|xphb\",\n      \"variantrule|resistance|xphb\",\n      \"variantrule|ritual|xphb\",\n      \"variantrule|round down|xphb\",\n      \"variantrule|save|xphb\",\n      \"variantrule|saving throw|xphb\",\n      \"variantrule|shape-shifting|xphb\",\n      \"variantrule|short rest|xphb\",\n      \"variantrule|simultaneous effects|xphb\",\n      \"variantrule|size|xphb\",\n      \"variantrule|skill|xphb\",\n      \"variantrule|speed|xphb\",\n      \"variantrule|spell|xphb\",\n      \"variantrule|spell attack|xphb\",\n      \"variantrule|spellcasting focus|xphb\",\n      \"variantrule|sphere [area of effect]|xphb\",\n      \"variantrule|stable|xphb\",\n      \"variantrule|stat block|xphb\",\n      \"variantrule|swim speed|xphb\",\n      \"variantrule|swimming|xphb\",\n      \"variantrule|target|xphb\",\n      \"variantrule|telepathy|xphb\",\n      \"variantrule|teleportation|xphb\",\n      \"variantrule|temporary hit points|xphb\",\n      \"variantrule|unarmed strike|xphb\",\n      \"variantrule|unoccupied space|xphb\",\n      \"variantrule|vulnerability|xphb\",\n      \"variantrule|weapon attack|xphb\",\n      \"variantrule|weapon|xphb\"\n    ],\n    \"aliases\": {\n      \"classtype|artificer|phb\": \"classtype|artificer|tce\",\n      \"item|alchemist's tools|phb\": \"item|alchemist's supplies|phb\",\n      \"item|alchemists' supplies|phb\": \"item|alchemist's supplies|phb\",\n      \"item|arrow of slaying (generic)|dmg\": \"item|arrow of slaying|dmg\",\n      \"item|backpack|dmg\": \"item|backpack|phb\",\n      \"item|breastplate|dmg\": \"item|breastplate|phb\",\n      \"item|caltrops (20)|phb\": \"item|caltrops (bag of 20)|phb\",\n      \"item|crysal ball|dmg\": \"item|crystal ball|dmg\",\n      \"item|everbright lantern|dmg\": \"item|everbright lantern|erlw\",\n      \"item|glassblower's tools|dmg\": \"item|glassblower's tools|phb\",\n      \"item|jeweler's tools|dmg\": \"item|jeweler's tools|phb\",\n      \"item|painter's tools|phb\": \"item|painter's supplies|phb\",\n      \"item|pale tincture (ingested)|dmg\": \"item|pale tincture|dmg\",\n      \"item|potion of speed|phb\": \"item|potion of speed|dmg\",\n      \"item|prosthetic limb|erlw\": \"item|prosthetic limb|tce\",\n      \"item|rope of climbing|xge\": \"item|rope of climbing|dmg\",\n      \"item|shield, +1|dmg\": \"item|+1 shield|dmg\",\n      \"item|thieves tools|phb\": \"item|thieves' tools|phb\",\n      \"item|wands of magic missiles|dmg\": \"item|wand of magic missiles|dmg\",\n      \"itemgroup|spell scroll|xphb\": \"itemgroup|spell scroll|xdmg\",\n      \"spell|acid arrow|phb\": \"spell|melf's acid arrow|phb\",\n      \"spell|bane spell|phb\": \"spell|bane|phb\",\n      \"spell|ceremony|phb\": \"spell|ceremony|xge\",\n      \"spell|charm monster|phb\": \"spell|charm monster|xge\",\n      \"spell|commpelled duel|phb\": \"spell|compelled duel|phb\",\n      \"spell|deception|phb\": \"skill|deception|phb\",\n      \"spell|detect good and evil|phb\": \"spell|detect evil and good|phb\",\n      \"spell|enlarge|phb\": \"spell|enlarge/reduce|phb\",\n      \"spell|fire wall|phb\": \"spell|wall of fire|phb\",\n      \"spell|frostbite|phb\": \"spell|frostbite|xge\",\n      \"spell|history|phb\": \"skill|history|phb\",\n      \"spell|infestation|phb\": \"spell|infestation|xge\",\n      \"spell|less restoration|phb\": \"spell|lesser restoration|phb\",\n      \"spell|phatasmal force|phb\": \"spell|phantasmal force|phb\",\n      \"spell|magic aura|phb\": \"spell|arcanist's magic aura|phb\",\n      \"spell|mind sliver|phb\": \"spell|mind sliver|tce\",\n      \"spell|ray of enfeeblemen t|phb\": \"spell|ray of enfeeblement|phb\",\n      \"spell|reduce|phb\": \"spell|enlarge/reduce|phb\",\n      \"spell|tenser 's floating disk|phb\": \"spell|tenser's floating disk|phb\",\n      \"spell|thunderclap|phb\": \"spell|thunderclap|xge\",\n      \"spell|toll the dead|phb\": \"spell|toll the dead|xge\",\n      \"spell|word of radiance|phb\": \"spell|word of radiance|xge\",\n      \"trap|quicksand|dmg\": \"hazard|quicksand|dmg\",\n      \"trapfluff|green slime|xdmg\": \"hazardfluff|green slime|xdmg\",\n      \"trapfluff|webs|xdmg\": \"hazardfluff|webs|xdmg\",\n      \"variantrule|weapon mastery properties|xphb\": \"reference|item mastery|xphb\"\n    },\n    \"fixes\": {\n      \"vehicles.json\": [\n        {\n          \"match\": \"(?<=\\\\\\\"source\\\\\\\": \\\\\\\"XDMG\\\\\\\",\\\\s*\\\\\\\"page\\\\\\\": \\\\d+,\\\\s*)\\\\\\\"srd\\\\\\\":\",\n          \"replace\": \"\\\"srd52\\\":\"\n        }\n      ],\n      \"book/book-xsac.json\": [\n        {\n          \"match\": \"the \\\\{@classFeature Protector\\\\|Cleric\\\\|XPHB\\\\|1\\\\|XPHB\\\\} option\",\n          \"replace\": \"the Protector option\"\n        }\n      ]\n    },\n    \"sourceFiles\": [\n      \"generated/gendata-tables.json\",\n      \"generated/gendata-variantrules.json\",\n      \"actions.json\",\n      \"adventures.json\",\n      \"backgrounds.json\",\n      \"bastions.json\",\n      \"bestiary\",\n      \"bestiary/legendarygroups.json\",\n      \"bestiary/template.json\",\n      \"books.json\",\n      \"class\",\n      \"conditionsdiseases.json\",\n      \"cultsboons.json\",\n      \"decks.json\",\n      \"deities.json\",\n      \"feats.json\",\n      \"fluff-backgrounds.json\",\n      \"fluff-bastions.json\",\n      \"fluff-conditionsdiseases.json\",\n      \"fluff-feats.json\",\n      \"fluff-items.json\",\n      \"fluff-languages.json\",\n      \"fluff-objects.json\",\n      \"fluff-optionalfeatures.json\",\n      \"fluff-races.json\",\n      \"fluff-rewards.json\",\n      \"fluff-trapshazards.json\",\n      \"fluff-vehicles.json\",\n      \"items-base.json\",\n      \"items.json\",\n      \"languages.json\",\n      \"magicvariants.json\",\n      \"objects.json\",\n      \"optionalfeatures.json\",\n      \"psionics.json\",\n      \"races.json\",\n      \"rewards.json\",\n      \"senses.json\",\n      \"skills.json\",\n      \"spells\",\n      \"tables.json\",\n      \"trapshazards.json\",\n      \"variantrules.json\",\n      \"vehicles.json\"\n    ],\n    \"indexes\": {\n      \"spell-source\": \"generated/gendata-spell-source-lookup.json\"\n    }\n  },\n  \"configPf2e\": {\n    \"templateKeys\": [\n      \"ability\",\n      \"action\",\n      \"affliction\",\n      \"archetype\",\n      \"background\",\n      \"book\",\n      \"creature\",\n      \"deity\",\n      \"feat\",\n      \"hazard\",\n      \"index\",\n      \"inline-ability\",\n      \"inline-affliction\",\n      \"inline-attack\",\n      \"item\",\n      \"note\",\n      \"ritual\",\n      \"spell\",\n      \"trait\"\n    ],\n    \"sourceFiles\": [\n      \"ancestries\",\n      \"backgrounds\",\n      \"bestiary\",\n      \"book\",\n      \"class\",\n      \"feats\",\n      \"items\",\n      \"spells\",\n      \"books.json\",\n      \"book/book-crb.json\",\n      \"items/baseitems.json\",\n      \"abilities.json\",\n      \"actions.json\",\n      \"afflictions.json\",\n      \"archetypes.json\",\n      \"companionsfamiliars.json\",\n      \"conditions.json\",\n      \"deities.json\",\n      \"domains.json\",\n      \"events.json\",\n      \"groups.json\",\n      \"hazards.json\",\n      \"languages.json\",\n      \"optionalfeatures.json\",\n      \"organizations.json\",\n      \"places.json\",\n      \"quickrules.json\",\n      \"relicgifts.json\",\n      \"rituals.json\",\n      \"skills.json\",\n      \"sources.json\",\n      \"tables.json\",\n      \"traits.json\",\n      \"variantrules.json\",\n      \"vehicles.json\"\n    ],\n    \"fixes\": {\n      \"actions.json\": [\n        {\n          \"match\": \"(\\\\{@condition [^|]+?\\\\|PC1})}\\\"\",\n          \"replace\": \"$1\\\"\"\n        },\n        {\n          \"match\": \"(connecting areas \\\\{@b A12}} and \\\\{@b A22})\",\n          \"replace\": \"(connecting areas {@b A12} and {@b A22})\"\n        }\n      ],\n      \"backgrounds/backgrounds-apg.json\": [\n        {\n          \"match\": \"\\\"\\\\(concentrate\\\\)\\\"\",\n          \"replace\": \"\\\"concentrate\\\"\"\n        },\n        {\n          \"match\": \"\\\"\\\\(concentrate\\\"\",\n          \"replace\": \"\\\"concentrate\\\"\"\n        },\n        {\n          \"match\": \"\\\"fortune\\\\)\\\"\",\n          \"replace\": \"\\\"fortune\\\"\"\n        }\n      ],\n      \"backgrounds/backgrounds-sog0.json\": [\n        {\n          \"match\": \"\\\\{@lore\\\\|\\\\|Farming Lore}\",\n          \"replace\": \"{@skill Lore||Farming Lore}\"\n        }\n      ],\n      \"bestiary/creatures-b2.json\": [\n        {\n          \"_comment\": \"Removable once https://github.com/Pf2eToolsOrg/Pf2eTools/pull/359 is in a tagged release\",\n          \"match\": \"\\\\{@skill Lore\\\\|Sailing Lore}\",\n          \"replace\": \"{@skill Lore||Sailing Lore}\"\n        }\n      ],\n      \"bestiary/creatures-lomm.json\": [\n        {\n          \"_comment\": \"Removable once https://github.com/Pf2eToolsOrg/Pf2eTools/pull/359 is in a tagged release\",\n          \"match\": \"\\\\{@condition drained <1>}\",\n          \"replace\": \"{@condition drained 1}\"\n        },\n        {\n          \"_comment\": \"Removable once https://github.com/Pf2eToolsOrg/Pf2eTools/pull/359 is in a tagged release\",\n          \"match\": \"\\\\{@condition drained <2>}\",\n          \"replace\": \"{@condition drained 2}\"\n        },\n        {\n          \"_comment\": \"Removable once https://github.com/Pf2eToolsOrg/Pf2eTools/pull/359 is in a tagged release\",\n          \"match\": \"\\\\{@condition clmusy\\\\|\\\\|clumsy 1}\",\n          \"replace\": \"{@condition clumsy 1}\"\n        }\n      ],\n      \"bestiary/creatures-lotxwg.json\": [\n        {\n          \"match\": \"\\\"\\\\{@ability swarm mind}\\\"\",\n          \"replace\": \"\\\"swarm mind\\\"\"\n        }\n      ],\n      \"bestiary/creatures-ec2.json\": [\n        {\n          \"_comment\": \"Removable once https://github.com/Pf2eToolsOrg/Pf2eTools/pull/359 is in a tagged release\",\n          \"match\": \"\\\\{@condition flat‐footed}\",\n          \"replace\": \"{@condition flat-footed}\"\n        }\n      ],\n      \"bestiary/creatures-av1.json\": [\n        {\n          \"_comment\": \"Removable once https://github.com/Pf2eToolsOrg/Pf2eTools/pull/359 is in a tagged release\",\n          \"match\": \"\\\\{@skill Astrology Lore}\",\n          \"replace\": \"{@skill Lore||Astrology Lore}\"\n        }\n      ],\n      \"bestiary/creatures-b3.json\": [\n        {\n          \"_comment\": \"Removable once https://github.com/Pf2eToolsOrg/Pf2eTools/pull/359 is in a tagged release\",\n          \"match\": \"\\\\{@condition persistent damage\\\\|\\\\|persistent cold damage} damage\\\\|\\\\|persistent cold damage}\",\n          \"replace\": \"{@condition persistent damage||persistent cold damage}\"\n        }\n      ],\n      \"bestiary/creatures-botd.json\": [\n        {\n          \"_comment\": \"Removable once https://github.com/Pf2eToolsOrg/Pf2eTools/pull/359 is in a tagged release\",\n          \"match\": \"\\\"name\\\": \\\"Throw \\\\{@dc 24} Fortitude;\\\"\",\n          \"replace\": \"\\\"name\\\": \\\"Throw\\\", \\\"dc\\\": 24, \\\"savingThrow\\\": \\\"Fortitude\\\"\"\n        }\n      ],\n      \"bestiary/creatures-sog1.json\": [\n        {\n          \"_comment\": \"Removable once https://github.com/Pf2eToolsOrg/Pf2eTools/pull/359 is in a tagged release\",\n          \"match\": \"\\\\{@action Recall Knowledge checks attempted\",\n          \"replace\": \"{@action Recall Knowledge} checks attempted\"\n        }\n      ],\n      \"bestiary/creatures-sog3.json\": [\n        {\n          \"match\": \"\\\\{@condition enfeebled 4\\\\.\",\n          \"replace\": \"{@condition enfeebled 4}.\"\n        }\n      ],\n      \"creatures-wow2.json\": [\n        {\n          \"match\": \"\\\\{@conditio n\",\n          \"replace\": \"{@condition \"\n        }\n      ],\n      \"deities.json\": [\n        {\n          \"match\": \"immobilizied\",\n          \"replace\": \"immobilized\"\n        },\n        {\n          \"match\": \"\\\"name\\\": \\\"\\\\{@i Ruyi Bang, the Extending Cudgel}\\\"\",\n          \"replace\": \"\\\"name\\\": \\\"Ruyi Bang, the Extending Cudgel\\\"\"\n        }\n      ],\n      \"domains.json\": [\n        {\n          \"match\": \"\\\\{\\n\\\\s+\\\"name\\\": \\\"Dreams\\\",\",\n          \"replace\": \"{ \\\"name\\\": \\\"Disorientation\\\", \\\"source\\\": \\\"LODM\\\", \\\"page\\\": 264, \\\"entries\\\": [ \\\"You can bewilder and perplex your foes.\\\" ] },{\\\"name\\\": \\\"Dreams\\\",\"\n        }\n      ],\n      \"feats/feats-apg.json\": [\n        {\n          \"match\": \"\\\\{@note \\\\{@sup 1}Mwangi Expanse, p.76}\",\n          \"replace\": \"{@sup 1}Mwangi Expanse, p.76\"\n        }\n      ],\n      \"feats/feats-loil.json\": [\n        {\n          \"match\": \"\\\\{@condition sustain a spell\\\\|\\\\|Sustaining the Spell}\",\n          \"replace\": \"{@action sustain a spell||Sustaining the Spell}\"\n        }\n      ],\n      \"feats/feats-pc1.json\": [\n        {\n          \"match\": \"(\\\\{@action [^|]+?\\\\|PC1})}\",\n          \"replace\": \"$1\"\n        },\n        {\n          \"match\": \"(\\\\{@condition [^|]+?\\\\|PC1}.*\\\\.)}\\\"\",\n          \"replace\": \"$1\\\"\"\n        },\n        {\n          \"match\": \"\\\\{@featGrimspawn\\\\|PC1}\",\n          \"replace\": \"{@feat Grimspawn|PC1}\"\n        },\n        {\n          \"match\": \"\\\\{@feathellspawn\\\\|PC1}\",\n          \"replace\": \"{@feat hellspawn|PC1} \"\n        },\n        {\n          \"match\": \"\\\\{@featpitborn\\\\|PC1}\",\n          \"replace\": \"{@feat pitborn|PC1} \"\n        },\n        {\n          \"match\": \"(\\\\{@skill [^|]+?\\\\|PC1})}\",\n          \"replace\": \"$1\"\n        }\n      ],\n      \"feats/feats-som.json\": [\n        {\n          \"match\": \"Searching}}\",\n          \"replace\": \"Searching}\"\n        }\n      ],\n      \"items/items-apg.json\": [\n        {\n          \"match\": \"\\\"Interact \",\n          \"replace\": \"\\\"{@action Interact} \"\n        }\n      ],\n      \"items/items-av1.json\": [\n        {\n          \"match\": \"\\\"Interact \\\\(emotion\\\",[\\\\s\\\\r\\\\n]+\\\"visual\\\\)\\\"\",\n          \"replace\": \"\\\"{@action Interact} ({@trait emotion}, {@trait visual})\\\"\"\n        },\n        {\n          \"match\": \"\\\"Interact \",\n          \"replace\": \"\\\"{@action Interact} \"\n        }\n      ],\n      \"items/items-av2.json\": [\n        {\n          \"match\": \"\\\"Interact \",\n          \"replace\": \"\\\"{@action Interact} \"\n        },\n        {\n          \"match\": \"\\\"activity\\\": \\\\{[\\\\s\\\\r\\\\n]+\\\"number\\\": 1,[\\\\s\\\\r\\\\n]+\\\"unit\\\": \\\"varies\\\",[\\\\s\\\\r\\\\n]+\\\"entry\\\": \\\"\\\\{@as 1} \\\\{@action Interact} \\\\(concentrate\\\\) Frequency\\\"[\\\\s\\\\r\\\\n]+},[\\\\s\\\\r\\\\n]+\\\"components\\\": [[\\\\s\\\\r\\\\n]+\\\"once per hour\\\"[\\\\s\\\\r\\\\n]+],\",\n          \"replace\": \"\\\"activity\\\": { \\\"number\\\": 1, \\\"unit\\\": \\\"action\\\" }, \\\"frequency\\\": { \\\"unit\\\": \\\"hour\\\", \\\"number\\\": 1 }, \\\"components\\\": [ \\\"{@action Interact} ({@trait concentrate})\\\" ],\"\n        }\n      ],\n      \"items/items-av3.json\": [\n        {\n          \"match\": \"\\\"Interact \",\n          \"replace\": \"\\\"{@action Interact} \"\n        }\n      ],\n      \"items/items-crb.json\": [\n        {\n          \"match\": \"\\\"Interact \",\n          \"replace\": \"\\\"{@action Interact} \"\n        },\n        {\n          \"match\": \"\\\\{@tanglefoot\\\\}\",\n          \"replace\": \"{@spell tanglefoot}\"\n        }\n      ],\n      \"items/items-ec4.json\": [\n        {\n          \"match\": \"\\\"Interact \",\n          \"replace\": \"\\\"{@action Interact} \"\n        }\n      ],\n      \"items/items-ec6.json\": [\n        {\n          \"match\": \"\\\"Interact \",\n          \"replace\": \"\\\"{@action Interact} \"\n        }\n      ],\n      \"items/items-g&g.json\": [\n        {\n          \"match\": \"\\\"Interact \",\n          \"replace\": \"\\\"{@action Interact} \"\n        }\n      ],\n      \"items/items-logm.json\": [\n        {\n          \"match\": \"\\\"Interact \",\n          \"replace\": \"\\\"{@action Interact} \"\n        }\n      ],\n      \"items/items-losk.json\": [\n        {\n          \"match\": \"(\\\"\\\\{@note.*?\\\\.)\\\"\\n\",\n          \"replace\": \"$1}\\\"\\n\"\n        }\n      ],\n      \"items/items-lotgb.json\": [\n        {\n          \"match\": \"\\\"Interact \\\\(concentrate\\\",[\\\\s\\\\r\\\\n]+\\\"manipulate\\\",[\\\\s\\\\r\\\\n]+\\\"metamagic\\\\)\\\"\",\n          \"replace\": \"\\\"{@action Interact} ({@trait concentrate}, {@trait manipulate}, {@trait metamagic})\\\"\"\n        },\n        {\n          \"match\": \"\\\"Interact \",\n          \"replace\": \"\\\"{@action Interact} \"\n        }\n      ],\n      \"items/items-sf3.json\": [\n        {\n          \"match\": \"@condition grappled}\",\n          \"replace\": \"@condition grabbed||grappled}\"\n        }\n      ],\n      \"rituals.json\": [\n        {\n          \"match\": \"@skill Religion \\\\(legendary\\\\)\",\n          \"replace\": \"@skill Religion} (legendary),\"\n        }\n      ],\n      \"spells/spells-logm.json\": [\n        {\n          \"match\": \"the target can't recover from the condition until they are cured}\",\n          \"replace\": \"the target can't recover from the condition until they are cured.\"\n        }\n      ],\n      \"spells/spells-pc1.json\": [\n        {\n          \"match\": \"\\\\{@b \\\\{@feat Elemental Shape\\\\|PC1} ([^.]+)\\\\.\",\n          \"replace\": \"{@b {@feat Elemental Shape|PC1}} $1.\"\n        },\n        {\n          \"match\": \"\\\\{@condition charmed\\\\|PC1}\",\n          \"replace\": \"charmed\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "src/main/resources/sourceMap.yaml",
    "content": "---\nconfig5e:\n  reference:\n    AAG:\n      name: \"Astral Adventurer's Guide\"\n      date: \"2022-08-16\"\n    AATM:\n      name: \"Adventure Atlas: The Mortuary\"\n      date: \"2023-10-17\"\n    ABH:\n      name: \"Astarion's Book of Hungers\"\n      date: \"2025-11-11\"\n    AI:\n      name: \"Acquisitions Incorporated\"\n      date: \"2019-06-18\"\n    AL:\n      name: \"Adventurers' League\"\n    ALCoS:\n      name: \"Adventurers League: Curse of Strahd\"\n      date: \"2016-03-15\"\n    ALEE:\n      name: \"Adventurers League: Elemental Evil\"\n      date: \"2015-04-07\"\n    ALRoD:\n      name: \"Adventurers League: Rage of Demons\"\n      date: \"2015-09-15\"\n    AWM:\n      name: \"Adventure with Muk\"\n      date: \"2019-11-12\"\n    AZfyT:\n      name: \"A Zib for your Thoughts\"\n      date: \"2019-03-05\"\n    AitFR:\n      name: \"Adventures in the Forgotten Realms\"\n      date: \"2021-06-30\"\n    AitFR-AVT:\n      name: \"Adventures in the Forgotten Realms: A Verdant Tomb\"\n      date: \"2021-07-14\"\n    AitFR-DN:\n      name: \"Adventures in the Forgotten Realms: Deepest Night\"\n      date: \"2021-07-21\"\n    AitFR-FCD:\n      name: \"Adventures in the Forgotten Realms: From Cyan Depths\"\n      date: \"2021-07-28\"\n    AitFR-ISF:\n      name: \"Adventures in the Forgotten Realms: In Scarlet Flames\"\n      date: \"2021-06-30\"\n    AitFR-THP:\n      name: \"Adventures in the Forgotten Realms: The Hidden Page\"\n      date: \"2021-07-07\"\n    BAM:\n      name: \"Boo's Astral Menagerie\"\n      date: \"2022-08-16\"\n    BGDIA:\n      name: \"Baldur's Gate: Descent Into Avernus\"\n      date: \"2019-09-17\"\n    BGG:\n      name: \"Bigby Presents: Glory of the Giants\"\n      date: \"2023-08-15\"\n    BMT:\n      name: \"The Book of Many Things\"\n      date: \"2023-11-14\"\n    BQGT:\n      name: \"Borderlands Quest: Goblin Trouble\"\n      date: \"2025-06-04\"\n    CaBoMP:\n      name: \"Crochet: A Book of Many Patterns\"\n      date: \"2026-03-31\"\n    CM:\n      name: \"Candlekeep Mysteries\"\n      date: \"2021-03-16\"\n    CRCotN:\n      name: \"Critical Role: Call of the Netherdeep\"\n      date: \"2022-03-15\"\n    CoA:\n      name: \"Chains of Asmodeus\"\n      date: \"2023-10-30\"\n    CoS:\n      name: \"Curse of Strahd\"\n      date: \"2016-03-15\"\n    DC:\n      name: \"Divine Contention\"\n      date: \"2019-06-24\"\n    DD:\n      name: \"Dangerous Designs\"\n      date: \"2020-03-17\"\n    DIP:\n      name: \"Dragon of Icespire Peak\"\n      date: \"2019-06-24\"\n    DMG:\n      name: \"Dungeon Master's Guide\"\n      date: \"2014-12-09\"\n    DMTCRG:\n      name: \"The Deck of Many Things: Card Reference Guide\"\n      date: \"2023-11-14\"\n    DSotDQ:\n      name: \"Dragonlance: Shadow of the Dragon Queen\"\n      date: \"2022-11-22\"\n    DitLCoT:\n      name: \"Descent into the Lost Caverns of Tsojcanth\"\n      date: \"2024-03-26\"\n    DoD:\n      name: \"Domains of Delight\"\n      date: \"2021-09-21\"\n    DoDk:\n      name: \"Dungeons of Drakkenheim\"\n      date: \"2023-12-21\"\n    DoSI:\n      name: \"Dragons of Stormwreck Isle\"\n      date: \"2022-07-31\"\n    DrDe:\n      name: \"Dragon Delves\"\n      date: \"2025-07-08\"\n    DrDe-ACfaS:\n      name: \"A Copper for a Song\"\n      date: \"2025-07-08\"\n    DrDe-BD:\n      name: \"A Copper for a Song\"\n      date: \"2025-07-08\"\n    DrDe-BtS:\n      name: \"Before the Storm\"\n      date: \"2025-07-08\"\n    DrDe-DaS:\n      name: \"Death at Sunset\"\n      date: \"2025-07-08\"\n    DrDe-DotSC:\n      name: \"Dragons of the Sandstone City\"\n      date: \"2025-07-08\"\n    DrDe-FWtVC:\n      name: \"For Whom the Void Calls\"\n      date: \"2025-07-08\"\n    DrDe-SD:\n      name: \"Shivering Death\"\n      date: \"2025-07-08\"\n    DrDe-TDoN:\n      name: \"The Dragon of Najkir\"\n      date: \"2025-07-08\"\n    DrDe-TFV:\n      name: \"The Forbidden Vale\"\n      date: \"2025-07-08\"\n    DrDe-TWoO:\n      name: \"The Will of Orcus\"\n      date: \"2025-07-08\"\n    EEPC:\n      name: \"Elemental Evil Player's Companion\"\n      date: \"2015-03-10\"\n    EET:\n      name: \"Elemental Evil: Trinkets\"\n      date: \"2015-03-10\"\n    EFA:\n      name: \"Eberron: Forge of the Artificer\"\n      date: \"2025-12-09\"\n    EFR:\n      name: \"Eberron: Forgotten Relics\"\n      date: \"2019-11-19\"\n    EGW:\n      name: \"Explorer's Guide to Wildemount\"\n      date: \"2020-03-17\"\n    EGW_DD:\n      name: \"Dangerous Designs\"\n      date: \"2020-03-17\"\n    EGW_FS:\n      name: \"Frozen Sick\"\n      date: \"2020-03-17\"\n    EGW_ToR:\n      name: \"Tide of Retribution\"\n      date: \"2020-03-17\"\n    EGW_US:\n      name: \"Unwelcome Spirits\"\n      date: \"2020-03-17\"\n    ERLW:\n      name: \"Eberron: Rising from the Last War\"\n      date: \"2019-11-19\"\n    ESK:\n      name: \"Essentials Kit\"\n      date: \"2019-06-24\"\n    FFotR:\n      name: \"Fated Flight of the Recluse\"\n      date: \"2025-12-09\"\n    FRAiF:\n      name: \"Forgotten Realms: Adventures in Faerûn\"\n      date: \"2025-11-11\"\n    FRAiF-TLLoL:\n      name: \"Forgotten Realms: The Lost Library of Lethchauntos\"\n      date: \"2025-11-11\"\n    FRHoF:\n      name: \"Forgotten Realms: Heroes of Faerûn\"\n      date: \"2025-11-11\"\n    FS:\n      name: \"Frozen Sick\"\n      date: \"2020-03-17\"\n    FTD:\n      name: \"Fizban's Treasury of Dragons\"\n      date: \"2021-11-26\"\n    GGR:\n      name: \"Guildmasters' Guide to Ravnica\"\n      date: \"2018-11-20\"\n    GHLoE:\n      name: \"Grim Hollow: Lairs of Etharis\"\n      date: \"2023-11-30\"\n    GoS:\n      name: \"Ghosts of Saltmarsh\"\n      date: \"2019-05-21\"\n    GotSF:\n      name: \"Giants of the Star Forge\"\n      date: \"2023-08-01\"\n    HAT-LMI:\n      name: \"Honor Among Thieves: Legendary Magic Items\"\n    HAT-TG:\n      name: \"Honor Among Thieves: Thieves' Gallery\"\n    HBTD:\n      name: \"Hold Back The Dead\"\n      date: \"2025-02-07\"\n    HF:\n      name: \"Heroes' Feast\"\n      date: \"2020-10-27\"\n    HFDoMM:\n      name: \"Heroes' Feast: The Deck of Many Morsels\"\n      date: \"2024-10-01\"\n    HFFotM:\n      name: \"Heroes' Feast Flavors of the Multiverse\"\n      date: \"2023-11-07\"\n    HFStCM:\n      name: \"Heroes' Feast: Saving the Children's Menu\"\n      date: \"2023-11-21\"\n    HWAitW:\n      name: \"Humblewood: Adventure in the Wood\"\n      date: \"2019-06-17\"\n    HWCS:\n      name: \"Humblewood Campaign Setting\"\n      date: \"2019-06-17\"\n    HftT:\n      name: \"Hunt for the Thessalhydra\"\n      date: \"2019-05-01\"\n    HoL:\n      name: \"The House of Lament\"\n      date: \"2021-05-18\"\n    HotB:\n      name: \"Heroes of the Borderlands\"\n      date: \"2025-09-16\"\n    HotDQ:\n      name: \"Hoard of the Dragon Queen\"\n      date: \"2014-08-19\"\n    IDRotF:\n      name: \"Icewind Dale: Rime of the Frostmaiden\"\n      date: \"2020-09-15\"\n    IMR:\n      name: \"Infernal Machine Rebuild\"\n      date: \"2019-11-12\"\n    JttRC:\n      name: \"Journeys through the Radiant Citadel\"\n      date: \"2022-07-19\"\n    KKW:\n      name: \"Krenko's Way\"\n      date: \"2018-11-20\"\n    KftGV:\n      name: \"Keys from the Golden Vault\"\n      date: \"2023-02-21\"\n    LFL:\n      name: \"Lorwyn: First Light\"\n      date: \"2025-11-18\"\n    LK:\n      name: \"Lightning Keep\"\n      date: \"2023-09-26\"\n    LLK:\n      name: \"Lost Laboratory of Kwalish\"\n      date: \"2018-11-10\"\n    LMoP:\n      name: \"Lost Mine of Phandelver\"\n      date: \"2014-07-15\"\n    LR:\n      name: \"Locathah Rising\"\n      date: \"2019-09-19\"\n    LRDT:\n      name: \"Red Dragon's Tale: A LEGO Adventure\"\n      date: \"2024-04-01\"\n    LoX:\n      name: \"Light of Xaryxis\"\n      date: \"2022-08-16\"\n    MCV1SC:\n      name: \"Monstrous Compendium Volume 1: Spelljammer Creatures\"\n      date: \"2022-04-21\"\n    MCV2DC:\n      name: \"Monstrous Compendium Volume 2: Dragonlance Creatures\"\n      date: \"2022-12-05\"\n    MCV3MC:\n      name: \"Monstrous Compendium Volume 3: Minecraft Creatures\"\n      date: \"2023-03-28\"\n    MCV4EC:\n      name: \"Monstrous Compendium Volume 4: Eldraine Creatures\"\n      date: \"2023-09-21\"\n    MFF:\n      name: \"Mordenkainen's Fiendish Folio\"\n      date: \"2019-11-12\"\n    MGELFT:\n      name: \"Muk's Guide To Everything He Learned From Tasha\"\n      date: \"2020-12-01\"\n    MM:\n      name: \"Monster Manual\"\n      date: \"2014-09-30\"\n    MOT:\n      name: \"Mythic Odysseys of Theros\"\n      date: \"2020-06-02\"\n    MPMM:\n      name: \"Mordenkainen Presents: Monsters of the Multiverse\"\n      date: \"2022-01-25\"\n    MPP:\n      name: \"Morte's Planar Parade\"\n      date: \"2023-10-17\"\n    MTF:\n      name: \"Mordenkainen's Tome of Foes\"\n      date: \"2018-05-29\"\n    MaBJoV:\n      name: \"Minsc and Boo's Journal of Villainy\"\n      date: \"2021-10-05\"\n    MisMV1:\n      name: \"Misplaced Monsters: Volume 1\"\n      date: \"2023-05-03\"\n    NF:\n      name: \"Netheril's Fall\"\n      date: \"2025-11-11\"\n    NRH:\n      name: \"NERDS Restoring Harmony\"\n      date: \"2021-09-01\"\n    NRH-ASS:\n      name: \"NERDS Restoring Harmony: A Sticky Situation\"\n      date: \"2021-09-01\"\n    NRH-AT:\n      name: \"NERDS Restoring Harmony: Adventure Together\"\n      date: \"2021-09-01\"\n    NRH-AVitW:\n      name: \"NERDS Restoring Harmony: A Voice in the Wilderness\"\n      date: \"2021-09-01\"\n    NRH-AWoL:\n      name: \"NERDS Restoring Harmony: A Web of Lies\"\n      date: \"2021-09-01\"\n    NRH-CoI:\n      name: \"NERDS Restoring Harmony: Circus of Illusions\"\n      date: \"2021-09-01\"\n    NRH-TCMC:\n      name: \"NERDS Restoring Harmony: The Candy Mountain Caper\"\n      date: \"2021-09-01\"\n    NRH-TLT:\n      name: \"NERDS Restoring Harmony: The Lost Tomb\"\n      date: \"2021-09-01\"\n    OGA:\n      name: \"One Grung Above\"\n      date: \"2017-10-11\"\n    OoW:\n      name: \"The Orrery of the Wanderer\"\n      date: \"2019-06-18\"\n    OotA:\n      name: \"Out of the Abyss\"\n      date: \"2015-09-15\"\n    PHB:\n      name: \"Player's Handbook\"\n      date: \"2014-08-19\"\n    PSA:\n      name: \"Plane Shift: Amonkhet\"\n      date: \"2017-07-06\"\n    PSD:\n      name: \"Plane Shift: Dominaria\"\n      date: \"2018-07-31\"\n    PSI:\n      name: \"Plane Shift: Innistrad\"\n      date: \"2016-07-12\"\n    PSK:\n      name: \"Plane Shift: Kaladesh\"\n      date: \"2017-02-16\"\n    PSX:\n      name: \"Plane Shift: Ixalan\"\n      date: \"2018-01-09\"\n    PSZ:\n      name: \"Plane Shift: Zendikar\"\n      date: \"2016-04-27\"\n    PaBTSO:\n      name: \"Phandelver and Below: The Shattered Obelisk\"\n      date: \"2023-09-19\"\n    PaF:\n      name: \"Puncheons and Flagons\"\n      date: \"2024-08-27\"\n    PiP:\n      name: \"Peril in Pinegrove\"\n      date: \"2023-11-20\"\n    PotA:\n      name: \"Princes of the Apocalypse\"\n      date: \"2015-04-07\"\n    QftIS:\n      name: \"Quests from the Infinite Staircase\"\n      date: \"2024-07-16\"\n    RMBRE:\n      name: \"The Lost Dungeon of Rickedness: Big Rick Energy\"\n      date: \"2019-11-19\"\n    RMR:\n      name: \"Dungeons & Dragons vs. Rick and Morty: Basic Rules\"\n      date: \"2019-11-19\"\n    RoT:\n      name: \"The Rise of Tiamat\"\n      date: \"2014-11-04\"\n    RoTOS:\n      name: \"The Rise of Tiamat Online Supplement\"\n      date: \"2014-11-04\"\n    RtG:\n      name: \"Return to Glory\"\n      date: \"2021-05-21\"\n    SAC:\n      name: \"Sage Advice Compendium\"\n      date: \"2019-01-31\"\n    SADS:\n      name: \"Sapphire Anniversary Dice Set\"\n      date: \"2019-12-12\"\n    SAiS:\n      name: \"Spelljammer: Adventures in Space\"\n      date: \"2022-08-16\"\n    SCAG:\n      name: \"Sword Coast Adventurer's Guide\"\n      date: \"2015-11-03\"\n    SCC:\n      name: \"Strixhaven: A Curriculum of Chaos\"\n      date: \"2021-12-07\"\n    SCC-ARiR:\n      name: \"A Reckoning in Ruins\"\n      date: \"2021-12-07\"\n    SCC-CK:\n      name: \"Campus Kerfuffle\"\n      date: \"2021-12-07\"\n    SCC-HfMT:\n      name: \"Hunt for Mage Tower\"\n      date: \"2021-12-07\"\n    SCC-TMM:\n      name: \"The Magister's Masquerade\"\n      date: \"2021-12-07\"\n    ScoEE:\n      name: \"Scions of Elemental Evil\"\n      date: \"2024-10-24\"\n    SCREEN:\n      name: \"Dungeon Master's Screen\"\n      date: \"2015-01-20\"\n    SCREEN_DUNGEON_KIT:\n      name: \"Dungeon Master's Screen: Dungeon Kit\"\n      date: \"2020-09-21\"\n    SCREEN_WILDERNESS_KIT:\n      name: \"Dungeon Master's Screen: Wilderness Kit\"\n      date: \"2020-11-17\"\n    SCREEN_SPELLJAMMER:\n      name: \"Dungeon Master's Screen: Spelljammer\"\n      date: \"2022-08-16\"\n    SDW:\n      name: \"Sleeping Dragon's Wake\"\n      date: \"2019-06-24\"\n    SKT:\n      name: \"Storm King's Thunder\"\n      date: \"2016-09-06\"\n    SLW:\n      name: \"Storm Lord's Wrath\"\n      date: \"2019-06-24\"\n    SatO:\n      name: \"Sigil and the Outlands\"\n      date: \"2023-10-17\"\n    SjA:\n      name: \"Spelljammer Academy\"\n      date: \"2022-07-11\"\n    TCE:\n      name: \"Tasha's Cauldron of Everything\"\n      date: \"2020-11-17\"\n    TD:\n      name: \"Tarot Deck\"\n      date: \"2022-05-24\"\n    TLK:\n      name: \"The Lost Kenku\"\n      date: \"2017-11-28\"\n    TTP:\n      name: \"The Tortle Package\"\n      date: \"2017-09-19\"\n    TftYP:\n      name: \"Tales from the Yawning Portal\"\n      date: \"2017-04-04\"\n    TftYP-AtG:\n      name: \"Tales from the Yawning Portal: Against the Giants\"\n      date: \"2017-04-04\"\n    TftYP-DiT:\n      name: \"Tales from the Yawning Portal: Dead in Thay\"\n      date: \"2017-04-04\"\n    TftYP-TFoF:\n      name: \"Tales from the Yawning Portal: The Forge of Fury\"\n      date: \"2017-04-04\"\n    TftYP-THSoT:\n      name: \"Tales from the Yawning Portal: The Hidden Shrine of Tamoachan\"\n      date: \"2017-04-04\"\n    TftYP-TSC:\n      name: \"Tales from the Yawning Portal: The Sunless Citadel\"\n      date: \"2017-04-04\"\n    TftYP-ToH:\n      name: \"Tales from the Yawning Portal: Tomb of Horrors\"\n      date: \"2017-04-04\"\n    TftYP-WPM:\n      name: \"Tales from the Yawning Portal: White Plume Mountain\"\n      date: \"2017-04-04\"\n    ToA:\n      name: \"Tomb of Annihilation\"\n      date: \"2017-09-19\"\n    ToB1-2023:\n      name: \"Tome of Beasts 1 (2023 Edition)\"\n      date: \"2023-05-31\"\n    ToD:\n      name: \"Tyranny of Dragons\"\n      date: \"2019-10-22\"\n    ToFW:\n      name: \"Turn of Fortune's Wheel\"\n      date: \"2023-10-17\"\n    ToR:\n      name: \"Tide of Retribution\"\n      date: \"2020-03-17\"\n    UATMC:\n      name: \"Unearthed Arcana: The Mystic Class\"\n      date: \"2017-03-13\"\n    US:\n      name: \"Unwelcome Spirits\"\n      date: \"2020-03-17\"\n    UtHftLH:\n      name: \"Uni and the Hunt for the Lost Horn\"\n      date: \"2024-09-24\"\n    VD:\n      name: \"Vecna Dossier\"\n      date: \"2022-06-09\"\n    VEoR:\n      name: \"Vecna: Eve of Ruin\"\n      date: \"2024-05-21\"\n    VGM:\n      name: \"Volo's Guide to Monsters\"\n      date: \"2016-11-15\"\n    VNotEE:\n      name: \"Vecna: Nest of the Eldritch Eye\"\n      date: \"2024-04-16\"\n    VRGR:\n      name: \"Van Richten's Guide to Ravenloft\"\n      date: \"2021-05-18\"\n    WBtW:\n      name: \"The Wild Beyond the Witchlight\"\n      date: \"2021-09-21\"\n    WDH:\n      name: \"Waterdeep: Dragon Heist\"\n      date: \"2018-09-18\"\n    WDMM:\n      name: \"Waterdeep: Dungeon of the Mad Mage\"\n      date: \"2018-11-20\"\n    WttHC:\n      name: \"Stranger Things: Welcome to the Hellfire Club\"\n      date: \"2025-10-07\"\n    XDMG:\n      name: \"Dungeon Master's Guide (2024)\"\n      date: \"2024-11-12\"\n    XGE:\n      name: \"Xanathar's Guide to Everything\"\n      date: \"2017-11-21\"\n    XMM:\n      name: \"Monster Manual (2024)\"\n      date: \"2025-02-18\"\n    XMtS:\n      name: \"X Marks the Spot\"\n      date: \"2017-12-11\"\n    XPHB:\n      name: \"Player's Handbook (2024)\"\n      date: \"2024-09-17\"\n    XSAC:\n      name: \"Sage Advice Compendium (2025)\"\n      date: \"2025-04-30\"\n    XScreen:\n      name: \"Dungeon Master's Screen (2024)\"\n      date: \"2024-11-12\"\n  longToAbv:\n    ALCurseOfStrahd: \"ALCoS\"\n    ALElementalEvil: \"ALEE\"\n    ALRageOfDemons: \"ALRoD\"\n    HEROES_FEAST: \"HF\"\n    PS-A: \"PSA\"\n    PS-D: \"PSD\"\n    PS-I: \"PSI\"\n    PS-K: \"PSK\"\n    PS-X: \"PSX\"\n    PS-Z: \"PSZ\"\n    SCC_ARiR: \"SCC-ARir\"\n    SCC_CK: \"SCC-CK\"\n    SCC_HfMT: \"SCC-HfMT\"\n    SCC_TMM: \"SCC-TMM\"\n    Screen: \"SCREEN\"\n    ScreenDungeonKit: \"SCREEN_DUNGEON_KIT\"\n    ScreenSpelljammer: \"SRC_SCREEN_SPELLJAMMER\"\n    ScreenWildernessKit: \"SCREEN_WILDERNESS_KIT\"\n    TYP: \"TftYP\"\n    TYP_AtG: \"TftYP-AtG\"\n    TYP_DiT: \"TftYP-DiT\"\n    TYP_TFoF: \"TftYP-TFoF\"\n    TYP_THSoT: \"TftYP-THSoT\"\n    TYP_TSC: \"TftYP-TSC\"\n    TYP_ToH: \"TftYP-ToHs\"\n    TYP_WPM: \"TftYP-WPM\"\n    UATheMysticClass: \"UATMC\"\n    freeRules2024: \"basicRules2024\"\nconfigPf2e:\n  reference:\n    \"7DfS0\":\n      name: \"Seven Dooms for Sandpoint Player's Guide\"\n      date: \"2024-03-08\"\n    AAWS:\n      name: \"Azarketi Ancestry Web Supplement\"\n      date: \"2021-02-24\"\n    AFFM:\n      name: \"A Few Flowers More\"\n      date: \"2023-07-23\"\n    AFoF:\n      name: \"A Fistful of Flowers\"\n      date: \"2022-07-25\"\n    APG:\n      name: \"Advanced Player's Guide\"\n      date: \"2020-07-30\"\n    AV0:\n      name: \"Abomination Vaults Player's Guide\"\n      date: \"2021-01-15\"\n    AV1:\n      name: \"Abomination Vaults #1: Ruins of Gauntlight\"\n      date: \"2021-01-15\"\n    AV2:\n      name: \"Abomination Vaults #2: Hands of the Devil\"\n      date: \"2021-02-24\"\n    AV3:\n      name: \"Abomination Vaults #3: Eyes of Empty Death\"\n      date: \"2021-04-07\"\n    AVH:\n      name: \"Abomination Vaults Hardcover\"\n      date: \"2022-05-25\"\n    AoA0:\n      name: \"Age of Ashes Player's Guide\"\n      date: \"2019-08-01\"\n    AoA1:\n      name: \"Age of Ashes #1: Hellknight Hill\"\n      date: \"2019-08-01\"\n    AoA2:\n      name: \"Age of Ashes #2: Cult of Cinders\"\n      date: \"2019-09-01\"\n    AoA3:\n      name: \"Age of Ashes #3: Tomorrow Must Burn\"\n      date: \"2019-09-18\"\n    AoA4:\n      name: \"Age of Ashes #4: Fires of the Haunted City\"\n      date: \"2019-10-16\"\n    AoA5:\n      name: \"Age of Ashes #5: Against the Scarlet Triad\"\n      date: \"2019-11-13\"\n    AoA6:\n      name: \"Age of Ashes #6: Broken Promises\"\n      date: \"2019-12-12\"\n    AoE0:\n      name: \"Agents of Edgewatch Player's Guide\"\n      date: \"2020-07-08\"\n    AoE1:\n      name: \"Agents of Edgewatch #1: Devil at the Dreaming Palace\"\n      date: \"2020-07-30\"\n    AoE2:\n      name: \"Agents of Edgewatch #2: Sixty Feet Under\"\n      date: \"2020-08-26\"\n    AoE3:\n      name: \"Agents of Edgewatch #3: All or Nothing\"\n      date: \"2020-09-15\"\n    AoE4:\n      name: \"Agents of Edgewatch #4: Assault on Hunting Lodge Seven\"\n      date: \"2020-10-14\"\n    AoE5:\n      name: \"Agents of Edgewatch #5: Belly of the Black Whale\"\n      date: \"2020-11-15\"\n    AoE6:\n      name: \"Agents of Edgewatch #6: Ruins of the Radiant Siege\"\n      date: \"2020-12-15\"\n    B1:\n      name: \"Bestiary\"\n      date: \"2019-08-01\"\n    B2:\n      name: \"Bestiary 2\"\n      date: \"2020-05-27\"\n    B3:\n      name: \"Bestiary 3\"\n      date: \"2021-04-07\"\n    BB:\n      name: \"Beginner Box\"\n      date: \"2020-11-11\"\n    BL0:\n      name: \"Blood Lords Player's Guide\"\n      date: \"2022-06-29\"\n    BL1:\n      name: \"Blood Lords #1: Zombie Feast\"\n      date: \"2022-07-27\"\n    BL2:\n      name: \"Blood Lords #2: Graveclaw\"\n      date: \"2022-08-31\"\n    BL3:\n      name: \"Blood Lords #3: Field of Maidens\"\n      date: \"2022-09-21\"\n    BL4:\n      name: \"Blood Lords #4: The Ghouls Hunger\"\n      date: \"2022-10-19\"\n    BL5:\n      name: \"Blood Lords #5: A Taste of Ashes\"\n      date: \"2022-11-16\"\n    BL6:\n      name: \"Blood Lords #6: Ghost King's Rage\"\n      date: \"2022-12-14\"\n    BotD:\n      name: \"Book of the Dead\"\n      date: \"2022-04-27\"\n    CC0:\n      name: \"Curtain Call Player's Guide\"\n      date: \"2024-07-10\"\n    CFD:\n      name: \"Critical Fumble Deck\"\n      date: \"2019-10-16\"\n    CHD:\n      name: \"Critical Hit Deck\"\n      date: \"2019-10-16\"\n    CotT:\n      name: \"Claws of the Tyrant\"\n      date: \"2025-04-02\"\n    CRB:\n      name: \"Core Rulebook\"\n      date: \"2019-08-01\"\n    DA:\n      name: \"Dark Archive\"\n      date: \"2022-07-27\"\n    DaLl:\n      name: \"Dinner at Lionlodge\"\n      date: \"2021-05-30\"\n    EC0:\n      name: \"Extinction Curse Player's Guide\"\n      date: \"2020-01-13\"\n    EC1:\n      name: \"Extinction Curse #1: The Show Must Go On\"\n      date: \"2020-01-30\"\n    EC2:\n      name: \"Extinction Curse #2: Legacy of the Lost God\"\n      date: \"2020-02-26\"\n    EC3:\n      name: \"Extinction Curse #3: Life's Long Shadows\"\n      date: \"2020-03-26\"\n    EC4:\n      name: \"Extinction Curse #4: Siege of the Dinosaurs\"\n      date: \"2020-04-29\"\n    EC5:\n      name: \"Extinction Curse #5: Lord of the Black Sands\"\n      date: \"2020-05-27\"\n    EC6:\n      name: \"Extinction Curse #6: The Apocalypse Prophet\"\n      date: \"2020-06-24\"\n    FRP0:\n      name: \"Fists of the Ruby Phoenix Player's Guide\"\n      date: \"2021-04-12\"\n    FRP1:\n      name: \"Fists of the Ruby Phoenix #1: Despair on Danger Island\"\n      date: \"2021-07-07\"\n    FRP2:\n      name: \"Fists of the Ruby Phoenix #2: Ready? Fight!\"\n      date: \"2021-07-07\"\n    FRP3:\n      name: \"Fists of the Ruby Phoenix #3: King of the Mountain\"\n      date: \"2021-07-07\"\n    FoP:\n      name: \"The Fall of Plaguestone\"\n      date: \"2019-08-01\"\n    G&G:\n      name: \"Guns & Gears\"\n      date: \"2021-10-13\"\n    GMG:\n      name: \"Gamemastery Guide\"\n      date: \"2020-02-26\"\n    GW0:\n      name: \"Gatewalkers Player's Guide\"\n      date: \"2023-01-10\"\n    GW1:\n      name: \"Gatewalkers #1: The Seventh Arch\"\n      date: \"2023-01-25\"\n    GW2:\n      name: \"Gatewalkers #2: They Watched the Stars\"\n      date: \"2023-02-22\"\n    GW3:\n      name: \"Gatewalkers #3: Dreamers of the Nameless Spires\"\n      date: \"2023-03-29\"\n    HPD:\n      name: \"Hero Point Deck\"\n      date: \"2021-11-10\"\n    HotW:\n      name: \"Howl of the Wild\"\n      date: \"2024-05-22\"\n    HStR:\n      name: \"Head-Shot the Rot\"\n      date: \"2021-10-21\"\n    LOACLO:\n      name: \"Lost Omens: Absalom, City of Lost Omens\"\n      date: \"2021-12-22\"\n    LOAG:\n      name: \"Lost Omens: Ancestry Guide\"\n      date: \"2021-02-24\"\n    LOCG:\n      name: \"Lost Omens: Character Guide\"\n      date: \"2019-10-16\"\n    LODM:\n      name: \"Lost Omens: Divine Mysteries\"\n      date: \"2024-11-20\"\n    LOGM:\n      name: \"Lost Omens: Gods & Magic\"\n      date: \"2020-01-29\"\n    LOGMWS:\n      name: \"Lost Omens: Gods & Magic Web Supplement\"\n      date: \"2020-01-29\"\n    LOHh:\n      name: \"Lost Omens: Highhelm\"\n      date: \"2023-06-28\"\n    LOIL:\n      name: \"Lost Omens: Impossible Lands\"\n      date: \"2021-11-06\"\n    LOKL:\n      name: \"Lost Omens: Knights of Lastwall\"\n      date: \"2022-05-25\"\n    LOL:\n      name: \"Lost Omens: Legends\"\n      date: \"2020-07-30\"\n    LOME:\n      name: \"Lost Omens: The Mwangi Expanse\"\n      date: \"2021-07-07\"\n    LOMM:\n      name: \"Lost Omens: Monsters of Myth\"\n      date: \"2021-12-22\"\n    LOPSG:\n      name: \"Lost Omens: Pathfinder Society Guide\"\n      date: \"2020-10-14\"\n    LORA:\n      name: \"Lost Omens: Rival Academies\"\n      date: \"2025-03-05\"\n    LOSK:\n      name: \"Lost Omens: Shining Kingdoms\"\n      date: \"2025-06-04\"\n    LOTG:\n      name: \"Lost Omens: Travel Guide\"\n      date: \"2022-08-31\"\n    LOTGB:\n      name: \"Lost Omens: The Grand Bazaar\"\n      date: \"2021-10-13\"\n    LOTXWG:\n      name: \"Lost Omens: Tian Xia World Guide\"\n      date: \"2024-04-24\"\n    LOWG:\n      name: \"Lost Omens: World Guide\"\n      date: \"2019-08-31\"\n    LTiBA:\n      name: \"Little Trouble in Big Absalom\"\n      date: \"2020-07-25\"\n    Mal:\n      name: \"Malevolence\"\n      date: \"2021-07-07\"\n    MotM:\n      name: \"Mark of the Mantis\"\n      date: \"2022-02-23\"\n    MS0:\n      name: \"Myth-Speaker Player's Guide\"\n      date: \"2025-06-16\"\n    NGD:\n      name: \"Night of the Gray Death\"\n      date: \"2021-10-13\"\n    OoA0:\n      name: \"Outlaws of Alkenstar Player's Guide\"\n      date: \"2022-03-28\"\n    OoA1:\n      name: \"Outlaws of Alkenstar #1: Punks in a Powder Keg\"\n      date: \"2022-04-27\"\n    OoA2:\n      name: \"Outlaws of Alkenstar #2: Cradle of Quartz\"\n      date: \"2022-05-25\"\n    OoA3:\n      name: \"Outlaws of Alkenstar #3: The Smoking Gun\"\n      date: \"2022-06-29\"\n    PC1:\n      name: \"Player Core\"\n      date: \"2023-11-15\"\n    PC2:\n      name: \"Player Core 2\"\n      date: \"2024-08-01\"\n    PFUM:\n      name: \"PATHFINDER: FUMBUS!\"\n      date: \"2021-11-11\"\n    POS1:\n      name: \"Pathfinder One-Shot: Sundered Waves\"\n      date: \"2021-03-06\"\n    QFF0:\n      name: \"Quest for the Frozen Flame Player's Guide\"\n      date: \"2021-12-20\"\n    QFF1:\n      name: \"Quest for the Frozen Flame #1: Broken Tusk Moon\"\n      date: \"2021-01-26\"\n    QFF2:\n      name: \"Quest for the Frozen Flame #2: Lost Mammoth Valley\"\n      date: \"2021-02-23\"\n    QFF3:\n      name: \"Quest for the Frozen Flame #3: Burning Tundra\"\n      date: \"2021-03-30\"\n    RoE:\n      name: \"Rage of Elements\"\n      date: \"2023-08-02\"\n    RotR0:\n      name: \"Revenge of the Runelords Player's Guide\"\n      date: \"2025-09-25\"\n    Rust:\n      name: \"Rusthenge\"\n      date: \"2023-10-18\"\n    SF0:\n      name: \"Stolen Fate Player's Guide\"\n      date: \"2023-04-13\"\n    SF1:\n      name: \"Stolen Fate #1: The Choosing\"\n      date: \"2023-04-26\"\n    SF2:\n      name: \"Stolen Fate #2: The Destiny War\"\n      date: \"2023-05-24\"\n    SF3:\n      name: \"Stolen Fate #3: Worst of All Possible Worlds\"\n      date: \"2023-06-28\"\n    SKT0:\n      name: \"Sky King's Tomb Player's Guide\"\n      date: \"2023-07-13\"\n    SaS:\n      name: \"Shadows at Sundown\"\n      date: \"2022-05-25\"\n    Sli:\n      name: \"The Slithering\"\n      date: \"2020-07-30\"\n    SoB0:\n      name: \"Shades of Blood Player's Guide\"\n      date: \"2025-03-27\"\n    SoG0:\n      name: \"Season of Ghosts Player's Guide\"\n      date: \"2023-10-02\"\n    SoG1:\n      name: \"Season of Ghosts #1: The Summer That Never Was\"\n      date: \"2023-10-18\"\n    SoG2:\n      name: \"Season of Ghosts #2: Let the Leaves Fall\"\n      date: \"2023-11-15\"\n    SoG3:\n      name: \"Season of Ghosts #3: No Breath to Cry\"\n      date: \"2023-12-23\"\n    SoG4:\n      name: \"Season of Ghosts #4: To Bloom Below the Web\"\n      date: \"2024-01-31\"\n    SoM:\n      name: \"Secrets of Magic\"\n      date: \"2021-09-01\"\n    SoT0:\n      name: \"Strength of Thousands Player's Guide\"\n      date: \"2021-07-26\"\n    SoT1:\n      name: \"Strength of Thousands #1: Kindled Magic\"\n      date: \"2021-08-05\"\n    SoT2:\n      name: \"Strength of Thousands #2: Spoken on the Song Wind\"\n      date: \"2021-09-01\"\n    SoT3:\n      name: \"Strength of Thousands #3: Hurricane's Howl\"\n      date: \"2021-10-13\"\n    SoT4:\n      name: \"Strength of Thousands #4: Secrets of the Temple-City\"\n      date: \"2021-10-13\"\n    SoT5:\n      name: \"Strength of Thousands #5: Doorway to the Red Star\"\n      date: \"2021-11-10\"\n    SoT6:\n      name: \"Strength of Thousands #6: Shadows of the Ancients\"\n      date: \"2021-07-26\"\n    SW0:\n      name: \"Spore War Player's Guide\"\n      date: \"2024-12-18\"\n    TEC:\n      name: \"The Enmity Cycle\"\n      date: \"2023-05-24\"\n    TV:\n      name: \"Treasure Vault\"\n      date: \"2023-02-22\"\n    TaL:\n      name: \"Torment and Legacy\"\n      date: \"2019-09-11\"\n    TiO:\n      name: \"Troubles in Otari\"\n      date: \"2020-12-09\"\n    ToK:\n      name: \"Threshold of Knowledge\"\n      date: \"2021-11-19\"\n    TotT0:\n      name: \"Triumph of the Tusk Player's Guide\"\n      date: \"2024-10-09\"\n    WoI:\n      name: \"War of Immortals\"\n      date: \"2024-10-30\"\n    WoW0:\n      name: \"Wardens of Wildwood Player's Guide\"\n      date: \"2024-04-23\"\n    WoW1:\n      name: \"Wardens of Wildwood #1: Pactbreaker\"\n      date: \"2024-04-23\"\n    WoW2:\n      name: \"Wardens of Wildwood #2: Severed at the Root\"\n      date: \"2024-05-22\"\n    WoW3:\n      name: \"Wardens of Wildwood #3: Shepherd of Decay\"\n      date: \"2024-06-26\"\n    WtD1:\n      name: \"Wake the Dead #1\"\n      date: \"2023-05-31\"\n    WtD2:\n      name: \"Wake the Dead #2\"\n      date: \"2023-07-26\"\n    WtD3:\n      name: \"Wake the Dead #3\"\n      date: \"2023-09-27\"\n    WtD4:\n      name: \"Wake the Dead #4\"\n      date: \"2023-11-28\"\n    WtD5:\n      name: \"Wake the Dead #5\"\n      date: \"2024-01-31\"\n  longToAbv:\n    GnG: \"G&G\"\n"
  },
  {
    "path": "src/main/resources/templates/README.md",
    "content": "# Default templates\n\n- [Templates for 5eTools data](./tools5e/)\n- [Templates for Pf2eTools data](./toolsPf2e/)\n"
  },
  {
    "path": "src/main/resources/templates/tools5e/README.md",
    "content": "---\ntype: fileIndex\n---\n# 5eTools default templates\n\nThis directory contains the default templates for 5eTools data.\n\n- [`background`](./background2md.txt)\n- [`bastion`](./bastion2md.txt)\n- [`class`](./class2md.txt)\n- [`css-font`](./css-font.txt)\n- [`deck`](./deck2md.txt)\n- [`deity`](./deity2md.txt)\n- [`feat`](./feat2md.txt)\n- [`hazard`](./hazard2md.txt)\n- [`index`](./index.txt)\n- [`item`](./item2md.txt)\n- [`monster`](./monster2md.txt)\n- [`note`](./note2md.txt)\n- [`object`](./object2md.txt)\n- [`psionic`](./psionic2md.txt)\n- [`race`](./race2md.txt)\n- [`reward`](./reward2md.txt)\n- [`spell`](./spell2md.txt)\n- [`subclass`](./subclass2md.txt)\n- [`vehicle`](./vehicle2md.txt)\n"
  },
  {
    "path": "src/main/resources/templates/tools5e/background2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-background\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n*Source: {resource.source}*  \n{#if resource.ability}\n\n**Ability Score Increase**: {resource.ability}\n{/if}{#if resource.prerequisite}\n\n***Prerequisites*** {resource.prerequisite}\n{/if}\n\n{resource.text}\n"
  },
  {
    "path": "src/main/resources/templates/tools5e/bastion2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-bastion\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n*{#if resource.prerequisite}Level {resource.level} {/if}Bastion facility*  \n\n{#if resource.prerequisite}\n- **Prerequisites**: {resource.prerequisite}\n{/if}{#if resource.space }\n- **Space**: {resource.spaceDescription}\n{/if}{#if resource.hirelings }\n- **Hirelings**: {resource.hirelingDescription}\n{/if}{#if resource.orders }\n- **{resource.orders.pluralizeLabel(\"Order\")}**: {resource.orders.join(\", \")}\n{/if}{#if resource.text }\n\n{resource.text}\n{/if}\n\n*Source: {resource.source}*\n"
  },
  {
    "path": "src/main/resources/templates/tools5e/class2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-class\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n*Source: {resource.source}*  \n\n{#if resource.classProgression }\n{resource.classProgression}\n\n{/if}\n## Hit Points\n\n{#if resource.hitDice }\n- **Hit Dice**: 1d{resource.hitDice} per {resource.name} level\n- **Hit Points at First Level:** {resource.hitDice} + CON\n- **Hit Points at Higher Levels:** add {resource.hitRollAverage} OR 1d{resource.hitDice} + CON  (minimum of 1)\n{#else}\n- **Hit Dice**: *x* = hit dice specified in the sidekick's statblock (human, gnome, kobold, etc.)\n- **Hit Points at First Level:** *x* + CON\n- **Hit Points at Higher Levels:** add 1d*x* + CON (minimum of 1)\n{/if}\n\n## Starting {resource.name}\n\n{resource.startingEquipment}\n\n{#if resource.multiclassing }\n## Multiclassing {resource.name}\n\n{resource.multiclassing}\n{/if}\n\n{resource.text}\n"
  },
  {
    "path": "src/main/resources/templates/tools5e/css-font.txt",
    "content": "@font-face {\n  font-family: \"{fontFamily}\";\n  font-style: normal;\n  font-display: swap;\n  font-weight: 400;\n  src: url(\"data:font/{type};charset=utf8;base64,{encoded}\");\n}\n"
  },
  {
    "path": "src/main/resources/templates/tools5e/deck2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-deck\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n*Source: {resource.source}*  \n\n{resource.text}\n\n## Cards\n\n{#for card in resource.cards}\n### {card.name}{#if card.face }\n{card.face.getEmbeddedLink(\"card\")}{/if}\n{card.text}\n\n{/for}\n"
  },
  {
    "path": "src/main/resources/templates/tools5e/deity2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-deity\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n*Source: {resource.source}* {#if resource.image}\n{resource.image.getEmbeddedLink(\"symbol\")}{/if}\n\n{#if resource.altNames }\n- **Alternate Names**: {#each resource.altNames}{it}{#if it_hasNext}, {/if}{/each}\n{/if}{#if resource.alignment }\n- **Alignment**: {resource.alignment}\n{/if}{#if resource.category }\n- **Category**: {resource.category}\n{/if}{#if resource.domains }\n- **Domains**: {resource.domains}\n{/if}{#if resource.pantheon }\n- **Pantheon**: {resource.pantheon}\n{/if}{#if resource.province }\n- **Province**: {resource.province}\n{/if}{#if resource.symbol }\n- **Symbol**: {resource.symbol}\n{/if}\n\n{resource.text}\n"
  },
  {
    "path": "src/main/resources/templates/tools5e/feat2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-feat\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}\n{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n*Source: {resource.source}*  \n\n{#if resource.level || resource.prerequisite}\n{#if resource.prerequisite}\n**Prerequisite**: {resource.prerequisite}\n{/if}{#if resource.level}\n**Level**: {resource.level}\n{/if}\n\n{/if}\n{resource.text}\n"
  },
  {
    "path": "src/main/resources/templates/tools5e/hazard2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-hazard\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n{#if resource.hazardType }*{resource.hazardType}*  \n{/if}{#if resource.text }\n\n{resource.text}\n{/if}\n\n{#if resource.source }*Source: {resource.source}* {/if}\n"
  },
  {
    "path": "src/main/resources/templates/tools5e/index.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-note\n- json5e-index\n---\n# Index of {name}\n\n{#for mapping in resources}\n- [{mapping.title}]({mapping.relativePath})\n{/for}\n"
  },
  {
    "path": "src/main/resources/templates/tools5e/item2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-item\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}\n{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n{#if resource.detail }*{resource.detail}*  {/if}\n\n{#if resource.prerequisite}\n- **Prerequisites**: {resource.prerequisite}\n{/if}{#if resource.armorClass }\n- **Armor Class**: {resource.armorClass}\n{#else if resource.damage }{#if resource.damage2h }\n- **Damage**:\n  - One-handed: {resource.damage}\n  - Two-handed: {resource.damage2h}\n{#else}\n- **Damage**: {resource.damage}\n{/if}{#if resource.range }\n- **Range**: {resource.range}\n{/if}{/if}{#if resource.properties }\n- **Properties**: {resource.properties}\n{/if}{#if resource.strengthRequirement }\n- **Strength**: Requires {resource.strengthRequirement} STR.\n{/if}{#if resource.stealthPenalty }\n- **Stealth**: The wearer has disadvantage on Stealth (DEX) checks.\n{/if}{#if resource.cost }\n- **Cost**: {resource.cost}\n{/if}{#if resource.weight }\n- **Weight**: {resource.weight} lbs.\n{/if}{#if resource.text }\n\n{resource.text}\n{/if}{#if resource.variants }\n\n**Variants**:\n{resource.variantSectionLinks}\n\n{#for variant in resource.variants}\n### {variant.name}\n\n{#if variant.prerequisite}\n- **Prerequisites**: {variant.prerequisite}\n{/if}{#if variant.armorClass }\n- **Armor Class**: {variant.armorClass}\n{#else if variant.damage }{#if variant.damage2h }\n- **Damage**:\n  - One-handed: {variant.damage}\n  - Two-handed: {variant.damage2h}\n{#else}\n- **Damage**: {variant.damage}\n{/if}{#if variant.range }\n- **Range**: {variant.range}\n{/if}{/if}{#if variant.properties }\n- **Properties**: {variant.properties}\n{/if}{#if variant.strengthRequirement }\n- **Strength**: Requires {variant.strengthRequirement} STR.\n{/if}{#if variant.stealthPenalty }\n- **Stealth**: The wearer has disadvantage on Stealth (DEX) checks.\n{/if}{#if variant.cost }\n- **Cost**: {variant.cost}\n{/if}{#if variant.weight }\n- **Weight**: {variant.weight} lbs.\n{/if}\n\n{/for}\n{/if}\n\n*Source: {resource.source}*\n"
  },
  {
    "path": "src/main/resources/templates/tools5e/monster2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-monster\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n*Source: {resource.source}*  \n\n{#if resource.description }\n{resource.description}\n\n{/if}{#if resource.hasSections }\n## Statblock\n\n{/if}\n```ad-statblock\ntitle: {resource.name}{#if resource.token}\n{resource.token.getEmbeddedLink(\"token\")}{/if}\n*{resource.size} {resource.fullType}, {resource.alignment}*\n\n{resource.acHp}\n- **Speed** {resource.speed}\n\n|STR|DEX|CON|INT|WIS|CHA|\n|:---:|:---:|:---:|:---:|:---:|:---:|\n|{resource.scores}|\n\n- **Proficiency Bonus** {resource.pb}\n- **Saving Throws** {#if resource.savingThrows }{resource.savingThrows}{#else}⏤{/if}\n- **Skills** {#if resource.skills }{resource.skills}{#else}⏤{/if}\n- **Senses** {#if resource.senses }{resource.senses}, {/if}passive Perception {resource.passive}\n{#if resource.immuneResist && resource.immuneResist.present }\n{resource.immuneResist}\n{/if}{#if resource.gear}\n- **Gear** {resource.gear.join(\", \")}\n{/if}\n- **Languages** {#if resource.languages }{resource.languages}{#else}—{/if}\n- **Challenge** {resource.cr}\n{#if resource.trait}\n\n## Traits\n{#for trait in resource.trait}\n\n{#if trait.name }***{trait.name}.*** {/if}{trait.desc}\n{/for}{/if}{#if resource.action}\n\n## Actions\n{#for action in resource.action}\n\n{#if action.name }***{action.name}.*** {/if}{action.desc}\n{/for}{/if}{#if resource.bonusAction}\n\n## Bonus Actions\n{#for bonusAction in resource.bonusAction}\n\n{#if bonusAction.name }***{bonusAction.name}.*** {/if}{bonusAction.desc}\n{/for}{/if}{#if resource.reaction}\n\n## Reactions\n{#for reaction in resource.reaction}\n\n{#if reaction.name }***{reaction.name}.*** {/if}{reaction.desc}\n{/for}{/if}{#if resource.legendary}\n\n## Legendary Actions\n{#for legendary in resource.legendary}\n\n{#if legendary.name }***{legendary.name}.*** {/if}{legendary.desc}\n{/for}{/if}{#if resource.legendaryGroupLink}\n\n!{resource.legendaryGroupLink}\n{/if}\n```\n^statblock\n{#if resource.environment }\n\n## Environment\n\n{resource.environment}\n{/if}\n"
  },
  {
    "path": "src/main/resources/templates/tools5e/note2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-note\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}{#if resource.source }\n*Source: {resource.source}* {/if}\n\n{resource.text}\n"
  },
  {
    "path": "src/main/resources/templates/tools5e/object2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-object\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n%%-- Embedded content starts on the next line. --%%\n*Source: {resource.source}*  \n\n{#if resource.text }\n{resource.text}\n\n{/if}{#if resource.hasSections }\n## Statblock\n\n{/if}\n```ad-statblock\ntitle: {resource.name}{#if resource.token}\n{resource.token.getEmbeddedLink(\"token\")}{/if}\n*{resource.size} {resource.objectType}{#if resource.creatureType } ({resource.creatureType}){/if}*\n\n{resource.acHp}\n- **Speed** {resource.speed}\n\n|STR|DEX|CON|INT|WIS|CHA|\n|:---:|:---:|:---:|:---:|:---:|:---:|\n|{resource.scores}|\n\n{#if resource.senses }\n- **Senses** {resource.senses}\n{/if}{#if resource.immuneResist && resource.immuneResist.present }\n{resource.immuneResist}\n{/if}{#if resource.action}\n\n## Actions\n{#for action in resource.action}\n\n{#if action.name }***{action.name}.*** {/if}{action.desc}\n{/for}{/if}\n```\n^statblock\n\n"
  },
  {
    "path": "src/main/resources/templates/tools5e/psionic2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-psionic\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n*{resource.typeOrder}*\n{#if resource.text }\n\n{resource.text}\n{/if}{#if resource.focus }\n\n**Psionic Focus.** {resource.focus}\n{/if}{#if resource.modes}\n\n## Modes\n{#for mode in resource.modes}\n\n{#if mode.name }***{mode.name}.*** {/if}{mode.desc}\n{/for}{/if}{#if resource.source }\n\n*Source: {resource.source}* {/if}\n"
  },
  {
    "path": "src/main/resources/templates/tools5e/race2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- {resource.cssClass}\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n*Source: {resource.source}*  \n\n- **Ability Scores**: {resource.ability}\n{#if resource.type}\n- **Type**: {resource.type}\n{/if}\n- **Size**: {resource.size}\n- **Speed**: {resource.speed}\n{#if resource.spellcasting}\n- **Spellcasting**: {resource.spellcasting}\n{/if}\n\n## Traits\n\n{resource.traits}\n{#if resource.description}\n\n## Description\n\n{resource.description}\n{/if}\n"
  },
  {
    "path": "src/main/resources/templates/tools5e/reward2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-reward\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n{#if resource.detail }*{resource.detail}*  \n{/if}{#if resource.signatureSpells }\n\n- **Signature Spells**: {resource.signatureSpells}\n{/if}{#if resource.text }\n\n{resource.text}\n{/if}\n\n*Source: {resource.source}* \n"
  },
  {
    "path": "src/main/resources/templates/tools5e/spell2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-spell\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n%%-- Embedded content starts on the next line. --%%\n*{resource.level}, {resource.school}{#if resource.ritual} (ritual){/if}*  \n\n- **Casting time:** {resource.time}{#if resource.ritual} unless cast as a ritual{/if}\n- **Range:** {resource.range}\n- **Components:** {resource.components}\n- **Duration:** {resource.duration}\n\n{resource.text}\n\n{#if resource.hasSections }\n## Summary\n\n{/if}{#if resource.classes }\n**Classes**: {resource.classes}\n\n{/if}\n*Source: {resource.source}*\n"
  },
  {
    "path": "src/main/resources/templates/tools5e/subclass2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-class\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n*{resource.parentClassLink}{#if resource.subclassTitle}: {resource.subclassTitle}{/if}*  \n*Source: {resource.source}*  \n\n{#if resource.classProgression }\n{resource.classProgression}\n\n{/if}\n\n{resource.text}\n"
  },
  {
    "path": "src/main/resources/templates/tools5e/vehicle2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses:\n- json5e-vehicle\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n%%-- Embedded content starts on the next line. --%%\n*Source: {resource.source}*  \n\n{#if resource.text && !resource.isObject }\n{resource.text}\n\n{/if}{#if resource.hasSections && !resource.isObject }\n## Statblock\n\n{/if}\n```ad-statblock\ntitle: {resource.name}{#if resource.token}\n{resource.token.getEmbeddedLink(\"token\")}{/if}\n*{resource.sizeDimension}; {resource.terrain}*\n{#if resource.shipCrewCargoPace}\n\n{resource.shipCrewCargoPace}\n{/if}{#if resource.isObject }{! ----- BEGIN OBJECT (type) ----- !}\n{#if resource.scores}\n\n|STR|DEX|CON|INT|WIS|CHA|\n|:---:|:---:|:---:|:---:|:---:|:---:|\n|{resource.scores}|\n\n{/if}{#if resource.immuneResist && resource.immuneResist.present }\n{resource.immuneResist}\n{/if}{#if resource.text }\n\n{resource.text}\n\n{/if}{#if resource.shipSections}{#each resource.shipSections}\n\n{it}\n{/each}{/if}\n{#else if resource.isCreature }{! ----- BEGIN CREATURE (type) ----- !}\n{#if resource.scores}\n\n|STR|DEX|CON|INT|WIS|CHA|\n|:---:|:---:|:---:|:---:|:---:|:---:|\n|{resource.scores}|\n\n{/if}{#if resource.immuneResist }\n{resource.immuneResist}\n{/if}{#if resource.shipSections}{#each resource.shipSections}\n\n{it}\n{/each}{/if}\n{#else if resource.isWarMachine }{! ----- BEGIN INFERNAL WAR MACHINE (type) ----- !}\n{#if resource.scores}\n\n|STR|DEX|CON|INT|WIS|CHA|\n|:---:|:---:|:---:|:---:|:---:|:---:|\n|{resource.scores}|\n\n{/if}{#if resource.immuneResist }\n{resource.immuneResist}\n{/if}{#if resource.shipSections}{#each resource.shipSections}\n\n{it}\n{/each}{/if}\n{#else if resource.isSpelljammer }{! ----- BEGIN SPELLJAMMER (type) ----- !}\n{#if resource.shipSections}{#each resource.shipSections}\n\n{it}\n{/each}{/if}\n{#else if resource.isShip }{! ----- BEGIN SHIP (type) ----- !}\n{#if resource.scores}\n\n|STR|DEX|CON|INT|WIS|CHA|\n|:---:|:---:|:---:|:---:|:---:|:---:|\n|{resource.scores}|\n\n{/if}{#if resource.immuneResist && resource.immuneResist.present }\n{resource.immuneResist}\n{/if}{#if resource.action}\n\n## Actions\n{#each resource.action}\n\n{it}\n{/each}{/if}{#if resource.shipSections}{#each resource.shipSections}\n\n{it}\n{/each}{/if}\n{/if}{! END SHIP (type) !}\n```\n^statblock\n\n"
  },
  {
    "path": "src/main/resources/templates/toolsPf2e/README.md",
    "content": "---\ntype: fileIndex\n---\n# Pf2eTools default templates\n\nThis directory contains the default templates for Pf2eTools data.\n\n- [`ability`](./ability2md.txt),\n- [`action`](./action2md.txt),\n- [`affliction`](./affliction2md.txt),\n- [`archetype`](./archetype2md.txt),\n- [`background`](./background2md.txt),\n- [`book`](./book2md.txt)\n- [`creature`](./creature2md.txt),\n- [`deity`](./deity2md.txt),\n- [`feat`](./feat2md.txt),\n- [`hazard`](./hazard2md.txt),\n- [`index`](./index.txt)\n- [`indexTrait`](./indexTrait.txt)\n- [`inline-ability`](./inline-ability2md.txt)\n- [`inline-affliction`](./inline-affliction2md.txt)\n- [`inline-attack`](./inline-attack2md.txt)\n- [`item`](./item2md.txt)\n- [`note`](./note2md.txt)\n- [`ritual`](./ritual2md.txt)\n- [`spell`](./spell2md.txt)\n- [`trait`](./trait2md.txt)\n"
  },
  {
    "path": "src/main/resources/templates/toolsPf2e/ability2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses: pf2e,pf2e-ability\n{#if resource.tags}\ntags:\n{#each resource.tags}\n- {it}\n{/each}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name} {resource.activity}\n{resource.traits join \"  \"}\n\n{#if resource.activity && resource.activity.text}\n- **Activate**: {resource.activity.text}{resource.components.join(\", \").prefixSpace}\n{/if}{#if resource.range}\n- **Range**: {resource.range}\n{/if}{#if resource.cost}\n- **Cost**: {resource.cost}\n{/if}{#if resource.frequency}\n- **Frequency**: {resource.frequency}\n{/if}{#if resource.trigger}\n- **Trigger**: {resource.trigger}\n{/if}{#if resource.requirements}\n- **Requirements**: {resource.requirements}\n{/if}{#if resource.prerequisites}\n- **Prerequisites**: {resource.prerequisites}\n{/if}{#if resource.hasAttributes}\n\n{/if}{#if resource.hasEffect}\n**Effect** {/if}{resource.text}\n{#if resource.special}\n\n{resource.special}\n{/if}{#if resource.source}\n\n*Source: {resource.source}*\n{/if}\n"
  },
  {
    "path": "src/main/resources/templates/toolsPf2e/action2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses: pf2e,pf2e-action\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}{#if resource.basic} (basic){/if}{#if resource.activity} {resource.activity}{/if}\n{#if resource.traits }{#each resource.traits}{it}  {/each}\n{/if}*Source: {resource.source}*  \n\n{#if resource.actionType }{resource.actionType}\n{/if}{#if resource.cost }\n- **Cost**: {resource.cost}\n{/if}{#if resource.prerequisites }\n- **Prerequisites**: {resource.prerequisites}\n{/if}{#if resource.frequency }\n- **Frequency**: {resource.frequency}\n{/if}{#if resource.trigger }\n- **Trigger**: {resource.trigger}\n{/if}{#if resource.requirements }\n- **Requirements**: {resource.requirements}\n{/if}{#if resource.activity }\n- **Activity**: {resource.activity.text}\n{/if}\n\n{resource.text}\n"
  },
  {
    "path": "src/main/resources/templates/toolsPf2e/affliction2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses: pf2e,pf2e-affliction\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}{#if resource.formattedLevel} *{resource.formattedLevel}*{/if}\n{#if resource.traits}{#each resource.traits}{it}  {/each}{/if}\n\n{#if resource.notes}\n{#each resource.notes}{it}{#if it_hasNext}, {/if}{/each}\n\n{/if}{#if resource.savingThrow}\n- **Saving Throws**: {resource.savingThrow}\n{/if}{#if resource.onset}\n- **Onset**: {resource.onset}\n{/if}{#if resource.maxDuration}\n- **Maximum Duration**: {resource.maxDuration}\n{/if}{#if resource.effect}\n\n**Effect** {resource.effect}\n{/if}{#if resource.text}\n\n{#if resource.hasSections}\n## Summary\n{/if}\n{resource.text}\n\n{/if}{#if resource.temptedCurse}\n\n## Tempting Curse\n\n{resource.temptedCurse}\n{/if}{#if resource.stages}\n\n## Stages\n\n{#each resource.stages}\n**{it.key}** {it.value.text}{#if it.value.duration } ({it.value.duration}){/if}  \n{/each}\n\n{/if}\n\n*Source: {resource.source}*\n"
  },
  {
    "path": "src/main/resources/templates/toolsPf2e/archetype2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses: pf2e,pf2e-archetype\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name} *Dedication Level {resource.dedicationLevel}*  \n{#if resource.traits }{#each resource.traits}{it}  {/each}\n{/if}   \n\n{resource.text}\n\n*Source: {resource.source}*{#if resource.feats }\n\n{#each resource.feats}\n{it}  \n\n{/each}{/if}\n"
  },
  {
    "path": "src/main/resources/templates/toolsPf2e/background2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses: pf2e,pf2e-background\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}\n*Source: {resource.source}*  \n\n{resource.text}\n"
  },
  {
    "path": "src/main/resources/templates/toolsPf2e/book2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses: pf2e,pf2e-book\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n{resource.text}\n"
  },
  {
    "path": "src/main/resources/templates/toolsPf2e/creature2md.txt",
    "content": "{#with resource}\n---\nobsidianUIMode: preview\ncssclasses: pf2e,pf2e-creature\n{#if tags}\ntags:\n{#each tags}\n- {it}\n{/each}\n{/if}\n{#if aliases}\naliases:\n{#each aliases}\n- {it}\n{/each}\n{/if}\n---\n# {name} *Creature {level}*  \n{traits join \" \"}\n\n``````ad-statblock-pf2e\n{#if perception}\n- **Perception** {perception.asBonus}; {senses join \", \"}\n{/if}{#if languages}\n- **Languages** {languages}\n{/if}{#if skills}\n- **Skills** {skills}\n{/if}{#if abilityMods}\n- {#each abilityMods.keys}**{it.capitalized}** {abilityMods.get(it).asBonus}{#if it_hasNext}, {/if}{/each}\n{/if}{#if items}\n- **Items** {items join \", \"}\n{/if}{#each abilities.top}\n{it}\n{/each}\n---\n{#if defenses}\n{defenses}\n{/if}{#each abilities.middle}\n{it}\n{/each}\n---\n- **Speed** {speed}\n{#for spells in spellcasting}\n- **{spells.name}** {spells.formattedStats}{#if spells.notes}, {spells.notes join \", \"}{/if}{!\n!}{#if spells.ranks}; {/if}{spells.ranks join \"; \"}{!\n!}{#if spells.constantRanks}; {/if}{#each spells.constantRanks}{!\n  !}**Constant ({it.rank})** {it.spells join \", \"}{#if it_hasNext}; {/if}{!\n!}{/each}\n{/for}{#for rituals in ritualCasting}\n- **{rituals.name}** {#if rituals.dc}DC {rituals.dc}; {/if}{rituals.ranks join \"; \"}\n{/for}{#each attacks}\n{it}  \n{/each}{#each abilities.bottom}\n{it}\n{/each}\n\n``````\n^statblock\n\n{#if hasSections}\n## Summary\n{/if}{#if description}\n{description}\n{/if}{#if text}\n{it}\n{/if}\n\n*Source: {source}*\n{/with}\n"
  },
  {
    "path": "src/main/resources/templates/toolsPf2e/deity2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses: pf2e,pf2e-deity\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}{#if resource.aliases } ({#each resource.aliases}{it}{#if it_hasNext}, {/if}{/each}){/if} *({resource.alignment})*  \n*Source: {resource.source}*  \n{#if resource.text }\n\n{resource.text}\n{/if}\n\n- **Category**: {resource.category}\n{#if resource.pantheon }\n- **Pantheon Members**: {resource.pantheon}\n{/if}{#if resource.edicts }\n- **Edicts** {resource.edicts}\n{/if}{#if resource.anathema }\n- **Anathema**: {resource.anathema}\n{/if}{#if resource.areasOfConcern }\n- **Areas of Concern**: {resource.areasOfConcern}\n{/if}{#if resource.followerAlignment }\n- **Follower Alignments**: {resource.followerAlignment}\n{/if}\n{#if resource.cleric }\n\n## Devotee benefits\n\n{resource.cleric}\n{/if}\n{#if resource.avatar}{#let avatar=resource.avatar}\n{#if avatar.preface}\n{avatar.preface}\n\n{/if}\n```ad-embed-avatar\ntitle: {avatar.name}\n\n{#if avatar.speed or avatar.shield}\n- {#if avatar.speed}Speed {avatar.speed}{#if avatar.shield}; {/if}{/if}{avatar.shield or ''}\n{/if}\n{#each avatar.attacks}\n- {it}\n{/each}{#each avatar.ability}\n- {it}\n{/each}\n```\n{/if}{#if resource.intercession }\n\n## Divine intercession  \n*Source: {resource.intercession.source}*\n\n{resource.intercession.flavor}\n\n{#if resource.intercession.minorBoon }\n- **Minor Boon** {resource.intercession.minorBoon}\n{/if}{#if resource.intercession.moderateBoon }\n- **Moderate Boon**: {resource.intercession.moderateBoon}\n{/if}{#if resource.intercession.majorBoon }\n- **Major Boon**: {resource.intercession.majorBoon}\n{/if}\n\n{#if resource.intercession.minorCurse }\n- **Minor Curse**: {resource.intercession.minorCurse}\n{/if}{#if resource.intercession.moderateCurse }\n- **Moderate Curse** {resource.intercession.moderateCurse}\n{/if}{#if resource.intercession.majorCurse }\n- **Major Curse**: {resource.intercession.majorCurse}\n{/if}\n{/if}\n"
  },
  {
    "path": "src/main/resources/templates/toolsPf2e/feat2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses: pf2e,pf2e-feat\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name} {#if resource.activity} {resource.activity}{/if} *Feat {resource.level}*  \n%%-- Embedded/inline feat starts on the next line. --%%\n{#if resource.traits }{#each resource.traits}{it}  {/each}\n{/if} \n\n{#if resource.note }\n{resource.note}\n\n{/if}{#if resource.access }\n- **Access**: {resource.access}\n{/if}{#if resource.prerequisites }\n- **Prerequisites**: {resource.prerequisites}\n{/if}{#if resource.frequency }\n- **Frequency**: {resource.frequency}\n{/if}{#if resource.trigger }\n- **Trigger** {resource.trigger}\n{/if}{#if resource.cost }\n- **Cost**: {resource.cost}\n{/if}{#if resource.requirements }\n- **Requirements**: {resource.requirements}\n{/if}{#if resource.activity }\n- **Activity** {resource.activity.text}\n{/if}\n\n{resource.text}\n\n{#if resource.special }\n**Special.** {resource.special}\n\n{/if}{#if resource.leadsTo }\n## {resource.name} leads to...\n\n{#each resource.leadsTo}{it}{#if it_hasNext}, {/if}{/each}\n\n{/if}{#if resource.hasSections && resource.source }\n## Summary\n\n{/if}{#if resource.source }\n*Source: {resource.source}*  {/if}{#if resource.embedded && resource.tags }\n%% {#each resource.tags}#{it} {/each}%%{/if}\n"
  },
  {
    "path": "src/main/resources/templates/toolsPf2e/hazard2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses: pf2e,pf2e-hazard\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name} *Hazard {resource.level}*  \n{#if resource.traits }{#each resource.traits}{it}  {/each}\n{/if} \n\n- **Complexity** {resource.complexity}\n{#if resource.stealth }\n- **Stealth** {resource.stealth}  \n{/if}\n\n{resource.text}\n\n{#if resource.disable }\n- **Disable** {resource.disable}  \n{/if}{#if resource.perception}\n- **Perception** {resource.perception.bonus}{#each resource.perception.notes} {it}{/each}\n{/if}{#if resource.abilities}\n\n{#each resource.abilities}{it}\n{/each}{/if}{#if resource.defenses }\n\n{resource.defenses}\n{/if}{#if resource.attacks}\n\n{#each resource.attacks}{it}  \n{/each}{/if}{#if resource.actions }\n\n{#each resource.actions}{it}\n{/each}{/if}{#if resource.routine }\n\n{resource.routineAdmonition}\n^routine\n{/if}{#if resource.reset }\n\n**Reset** {resource.reset}  \n{/if}{#if resource.hasSections }\n\n## Summary\n{/if}\n\n*Source: {resource.source}*\n"
  },
  {
    "path": "src/main/resources/templates/toolsPf2e/index.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses: pf2e,pf2e-index\n---\n# Index of {name}\n\n{#for mapping in resources}\n- [{mapping.title}]({mapping.relativePath})\n{/for}\n"
  },
  {
    "path": "src/main/resources/templates/toolsPf2e/indexTrait.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses: pf2e,pf2e-index\n---\n# Index of Traits\n\n{#each resource.categoryLinks}{it}{#if it_hasNext}, {/if}{/each}\n\n{#for entry in resource.categoryToTraits}\n\n## {entry.key}\n\n{#for element in entry.value}\n- {element}\n{/for}\n{/for}\n"
  },
  {
    "path": "src/main/resources/templates/toolsPf2e/inline-ability2md.txt",
    "content": "```ad-embed-ability\n{#if resource.hasDetails }\ntitle: **{#if resource.reference}{resource.reference}{#else}{resource.name}{/if}** {resource.activity}{resource.components.join(\", \").prefixSpace}{#if resource.traits} ({resource.bareTraitList}){/if}\n{#if resource.note}\n> [!pf2-note] \n> {resource.note}\n{/if}{#if resource.range}\n- **Range**: {resource.range}\n{/if}{#if resource.cost}\n- **Cost**: {resource.cost}\n{/if}{#if resource.frequency}\n- **Frequency**: {resource.frequency}\n{/if}{#if resource.trigger}\n- **Trigger**: {resource.trigger}\n{/if}{#if resource.requirements}\n- **Requirements**: {resource.requirements}\n{/if}{#if resource.prerequisites}\n- **Prerequisites**: {resource.prerequisites}\n{/if}{#if resource.hasAttributes}\n\n{/if}{#if resource.hasEffect}\n**Effect** {/if}{resource.text}  \n{#if resource.special}\n\n**Special**: {resource.special}  \n{/if}{#if resource.source || resource.tags}\n%%\n{#if resource.source}\nSource: {resource.source}*  \n{/if}\n{#each resource.tags} #{it} {/each}\n%%{/if}\n{#else}\ntitle: **{resource.name}** {resource.text}\n{/if}\n```\n"
  },
  {
    "path": "src/main/resources/templates/toolsPf2e/inline-affliction2md.txt",
    "content": "````ad-inline-affliction\n{#if resource.name}\ntitle: {resource.name}{#if resource.formattedLevel} _{resource.formattedLevel}_{/if}\n{/if}\n\n{#if resource.traits}\n{#each resource.traits}{it}  {/each}\n{/if}{#if resource.text}\n{resource.text}\n\n{/if}{#if resource.notes}\n{#each resource.notes}{it}{#if it_hasNext}, {/if}{/each}\n\n{/if}{#if resource.savingThrow}\n- **Saving Throws**: {resource.savingThrow}\n{/if}{#if resource.onset}\n- **Onset**: {resource.onset}\n{/if}{#if resource.maxDuration}\n- **Maximum Duration**: {resource.maxDuration}  \n{/if}{#if resource.effect}\n\n**Effect** {resource.effect}\n{/if}{#if resource.stages}\n\n## Stages\n\n{#each resource.stages}\n**{it.key}** {it.value.text}{#if it.value.duration } ({it.value.duration}){/if}  \n{/each}\n{/if}{#if resource.source}\n*Source: {resource.source}*\n{/if}\n{#if resource.tags}%% {#each resource.tags}#{it} {/each}%%{/if}\n````\n"
  },
  {
    "path": "src/main/resources/templates/toolsPf2e/inline-attack2md.txt",
    "content": "{#let r=resource}\n{#if r.multilineEffect}\n```ad-inline-attack\ntitle: {r.rangeType} {r.activity} {r.name.capitalized}{r.bonus.prefixSpace}{r.formattedTraits.prefixSpace}\n{#if r.damage}\n**Damage** {r.damage}  \n{/if}{#if r.multilineEffect}\n**Effect** {r.multilineEffect}  \n{/if}\n```\n{#else}\n- **{r.rangeType}** {r.activity} {r.name}{r.bonus.prefixSpace}{r.formattedTraits.prefixSpace}{#if r.damage}, **Damage** {r.damage}{/if}\n{/if}\n"
  },
  {
    "path": "src/main/resources/templates/toolsPf2e/item2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses: pf2e,pf2e-item\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}{#if resource.level } *Item {resource.level}*{/if}  \n{#if resource.traits }{#each resource.traits}{it}  {/each}\n{/if} \n\n{#if resource.access }\n- **Access** {resource.access}\n{/if}{#if resource.price }\n- **Price** {resource.price}\n{/if}{#if resource.craftReq }\n- **Craft Requirements** {resource.craftReq}\n{/if}{#if resource.ammunition }\n- **Ammunition** {resource.ammunition}\n{/if}{#if resource.contract }\n- {#each resource.contract}{it}{#if it_hasNext}; {/if}{/each}\n{/if}{#if resource.usage }\n- {#each resource.usage}{it}{#if it_hasNext}; {/if}{/each}\n{/if}{#if resource.duration }\n- **Duration** {resource.duration}\n{/if}{#if resource.activate }\n- **Activate** {resource.activate}\n{/if}{#if resource.onset }\n- **Onset** {resource.onset}\n{/if}{#if resource.shield }\n{resource.shield}\n{/if}{#if resource.armor }\n{resource.armor}\n{/if}{#if resource.weapons }\n{#each resource.weapons}{it}\n{/each}{/if}{#if resource.hands }\n- **Hands** {resource.hands}\n{/if}{#if resource.category || resource.group }\n- {#if resource.category }**Category** {resource.category}{#if resource.group}; {/if}{/if}{#if resource.group }**Group** {resource.group} {/if}\n{/if}\n\n{resource.text}\n\n{#if resource.variants }\n---\n\n### Variants\n{#for variant in resource.variants }\n\n{variant}\n{/for}{/if}\n\n{#if resource.source }\n---\n*Source: {resource.source}*  {/if}\n"
  },
  {
    "path": "src/main/resources/templates/toolsPf2e/note2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses: pf2e,pf2e-note\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}  \n{#if resource.source }*Source: {resource.source}*  {/if}\n\n{resource.text}\n"
  },
  {
    "path": "src/main/resources/templates/toolsPf2e/ritual2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses: pf2e,pf2e-ritual\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name} *{resource.ritualType} {resource.level}*  \n{#if resource.traits }{#each resource.traits}{it}  {/each}\n{/if}  \n\n- {resource.casting}\n{#if resource.checks }\n- {resource.checks}\n{/if}{#if resource.targeting }\n- {resource.targeting}\n{/if}{#if resource.requirements }\n- **Requirements**: {resource.requirements}\n{/if}\n\n{resource.text}\n\n{#if resource.heightened }\n{#each resource.heightened}{it}\n\n{/each}{/if}{#if resource.hasSections }\n\n## Summary\n\n{/if}\n*Source: {resource.source}*\n"
  },
  {
    "path": "src/main/resources/templates/toolsPf2e/spell2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses: pf2e,pf2e-spell\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name} *{resource.spellType} {resource.level}*   \n{#if resource.traits }{#each resource.traits}{it}  {/each}\n{/if}  \n\n{#if resource.traditions }\n- **Traditions** {#each resource.traditions}{it}{#if it_hasNext}, {/if}{/each}\n{/if}{#if resource.domains }\n- **Domains** {#each resource.domains}{it}{#if it_hasNext}, {/if}{/each}\n{/if}{#if resource.subclass }\n- {#each resource.subclass}**{it.category}** {it.text}{#if it_hasNext}, {/if}{/each}\n{/if}\n- **Cast**: {resource.castDuration}{#if resource.components} ({resource.formattedComponents}){/if}\n{#if resource.cost}\n- **Cost**: {resource.cost}\n{/if}{#if resource.trigger}\n- **Trigger**: {resource.trigger}\n{/if}{#if resource.requirements}\n- **Requirements**: {resource.requirements}\n{/if}{#if resource.targeting}\n- {resource.targeting}\n{/if}{#if resource.save and !resource.save.hidden}\n- **Saving Throw**: {resource.save}\n{/if}{#if resource.duration}\n- **Duration**: {resource.duration}\n{/if}\n\n{resource.text}\n\n{#if resource.heightened }\n{#each resource.heightened}{it}\n\n{/each}{/if}\n{#if resource.amp }\n## Amp\n\n{resource.amp}\n\n{/if}\n{#if resource.hasSections }\n## Summary\n\n{/if}\n{#if resource.spellLists }\n**Spell Lists**: {#each resource.spellLists}{it}{#if it_hasNext}, {/if}{/each}\n\n{/if}\n*Source: {resource.source}*\n"
  },
  {
    "path": "src/main/resources/templates/toolsPf2e/trait2md.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses: pf2e,pf2e-trait\n{#if resource.tags }\ntags:\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# {resource.name}  \n*Source: {resource.source}*  \n\n{resource.text}\n\n{#if resource.categories }\n- **Categories**: {#each resource.categories}{it}{#if it_hasNext}, {/if}{/each}\n{/if}\n"
  },
  {
    "path": "src/scss/dnd5e/_admonitions.scss",
    "content": "body {\n  --admonition-charm: 211,141,159;\n  --admonition-charm-text: var(--admonition-charm);\n  --admonition-letter: 98, 159, 197;\n  --admonition-npc: 102, 121, 137;\n  --admonition-scene: 139, 167, 145;\n  --admonition-skill: 236,201,134;\n  --admonition-skill-text: var(--admonition-skill);\n  --admonition-weather: 53,119,174;\n  --admonition-flowchart: 72,72,72;\n}\n.theme-light {\n  --admonition-charm: 222,170,184;\n  --admonition-charm-text: 167,92,112;\n  --admonition-npc: 58, 125, 127;\n  --admonition-scene: 92, 122, 99;\n  --admonition-skill: 221,178,84;\n  --admonition-skill-text: 157,101,83;\n}\n.callout[data-callout=\"charm\"] {\n  --callout-color: var(--admonition-charm);\n  --callout-title-color: rgb(var(--admonition-charm-text));\n  .callout-title {\n    color: var(--callout-title-color);\n  }\n}\n.callout[data-callout=\"letter\"] {\n  --callout-color: var(--admonition-letter);\n}\n.callout[data-callout=\"npc\"] {\n  --callout-color: var(--admonition-npc);\n}\n.callout[data-callout=\"readaloud\"],\n.callout[data-callout=\"scene\"] {\n  --callout-color: var(--admonition-scene);\n}\n.callout[data-callout=\"skill\"] {\n  --callout-color: var(--admonition-skill);\n  --callout-title-color: rgb(var(--admonition-skill-text));\n}\n.callout[data-callout=\"weather\"] {\n  --callout-color: var(--admonition-weather);\n}\n.callout[data-callout=\"flowchart\"] {\n  --callout-color: var(--admonition-flowchart);\n  --callout-border-width: 0.10rem;\n}\n.callout[data-callout^=\"embed-\"] {\n  --embed-border-left: none;\n  --embed-border-right: none;\n  --embed-padding: 0;\n}\n.json5e-background,\n.json5e-class,\n.json5e-deck,\n.json5e-deity,\n.json5e-feat,\n.json5e-hazard,\n.json5e-item,\n.json5e-monster,\n.json5e-note,\n.json5e-object,\n.json5e-psionic,\n.json5e-race,\n.json5e-reward,\n.json5e-spell,\n.json5e-vehicle  {\n  div:has(> .callout[data-callout=\"flowchart\"]):has(+ div > .callout[data-callout=\"flowchart\"]) {\n    position: relative;\n\n    &:after {\n      content: '\\2193';\n      color: var(--admonition-flowchart);\n      display: block;\n      position: absolute;\n      bottom: -10px;\n      left: 50%;\n      margin-left: 7px;\n      width: 14px;\n      height: 14px;\n      font-size: 14px;\n      text-align: center;\n    }\n  }\n  .callout[data-callout=\"gallery\"] {\n    --callout-color: transparent;\n    --callout-border-width: 0;\n    .callout-content {\n      p {\n        display: flex;\n        flex-wrap: wrap;\n        justify-content: space-evenly;\n        align-content: center;\n      }\n\n      span[src$=\"#gallery\"],\n      div[src$=\"#gallery\"] {\n        max-width: 49%;\n        img {\n          max-height: 40vh;\n        }\n      }\n    }\n    .callout-title {\n      display: none;\n    }\n  }\n}\n"
  },
  {
    "path": "src/scss/dnd5e/_float-images-mixin.scss",
    "content": "@mixin floatBySrcAnchor() {\n  div,\n  span,\n  span.internal-embed,\n  img {\n    &.image-embed {\n      position: relative;\n\n      /* Display the alt text after the image */\n      /*\n      &::after {\n        content: attr(alt);\n        display: block;\n        margin-top: 0.5em;\n        top: 100%; // Positions it below the parent element\n        left: 0;\n      }\n      */\n    }\n    &[src$=\"#center\"] {\n      display: flex;\n      width: 100%;\n      justify-content: center;\n      align-items: center;\n    }\n    &[src$=\"#card\"],\n    &[src$=\"#symbol\"],\n    &[src$=\"#portrait\"],\n    &[src$=\"#token\"],\n    &[src$=\"#right\"] {\n      float: right;\n      padding-left: 5px;\n    }\n    &[src$=\"#left\"] {\n      float: left;\n      padding-right: 5px;\n    }\n    &[src$=\"#left\"],\n    &[src$=\"#center\"],\n    &[src$=\"#right\"] {\n      img {\n        max-height: 60vh;\n      }\n    }\n    &[src$=\"#left\"],\n    &[src$=\"#right\"] {\n      max-width: 50%;\n    }\n    &[src$=\"#card\"],\n    &[src$=\"#token\"] {\n      width: 150px;\n    }\n    &[src$=\"#symbol\"],\n    &[src$=\"#portrait\"] {\n      width: 200px;\n    }\n  }\n}\np,\ndiv,\nspan {\n  div.image-embed,\n  span.image-embed,\n  img {\n    &:hover::after {\n      content: attr(alt);\n      position: absolute;\n      background: rgba(0, 0, 0, 0.8);\n      color: white;\n      padding: 4px 8px;\n      border-radius: 4px;\n      font-size: 0.8em;\n      width: 200px;\n      max-width: 200px;\n      z-index: 10;\n      top: 2em;\n      left: 50%;\n      transform: translateX(-50%); // Centers it horizontally\n    }\n  }\n}\n"
  },
  {
    "path": "src/scss/dnd5e/_float-images.scss",
    "content": "@use \"float-images-mixin\" as *;\n\n.json5e-background,\n.json5e-class,\n.json5e-deck,\n.json5e-deity,\n.json5e-feat,\n.json5e-hazard,\n.json5e-item,\n.json5e-monster,\n.json5e-note,\n.json5e-object,\n.json5e-psionic,\n.json5e-race,\n.json5e-reward,\n.json5e-species,\n.json5e-spell,\n.json5e-vehicle {\n  &.markdown-preview-view,\n  &.markdown-source-view {\n    @include floatBySrcAnchor();\n  }\n}\n/* For decks of cards, ensure images don't overlap */\n.json5e-deck {\n  h3 {\n    clear: both;\n  }\n}\n"
  },
  {
    "path": "src/scss/dnd5e/_no-inline-title.scss",
    "content": "// Suppress inline title\n.json5e-background,\n.json5e-class,\n.json5e-deck,\n.json5e-deity,\n.json5e-feat,\n.json5e-hazard,\n.json5e-item,\n.json5e-monster,\n.json5e-note,\n.json5e-object,\n.json5e-psionic,\n.json5e-race,\n.json5e-reward,\n.json5e-spell,\n.json5e-vehicle {\n  .inline-title {\n    display: none;\n  }\n}\n"
  },
  {
    "path": "src/scss/dnd5e/_statblock.scss",
    "content": "// Overwriting Tweaks for 5e Statblocks\nbody {\n  --statblock-accent: 201,60,60;\n}\n\n// force resolution of floating tokens or other images ahead of statblock\n.admonition-statblock-parent {\n  clear: both;\n}\n.callout[data-callout=\"statblock\"] {\n  --callout-color: var(--statblock-accent);\n\n  .callout-title {\n    line-height: var(--line-height);\n\n    .callout-title-content {\n      flex: 2;\n      font-size: var(--h3-size);\n    }\n  }\n\n  .callout-content {\n\n    > :first-child {\n      margin-top: 0.5em;\n    }\n\n    > :last-child {\n      margin-bottom: 0.5em;\n    }\n\n    h1,\n    h2,\n    h3 {\n      font-family: var(--default-font);\n      font-variant: common-ligatures small-caps;\n    }\n\n    h1 {\n      font-size: 1.4em;\n      line-height: 1.4em;\n      margin: 0;\n      padding: 0;\n    }\n\n    h2,\n    h3 {\n      font-size: 1.2em;\n      line-height: 1.2em;\n      padding: .5em 0 0 0;\n      margin-top: .2em;\n      margin-bottom: .3em;\n      border-bottom: 1px solid rgb(var(--statblock-accent));\n    }\n\n    p {\n      line-height: 1.2em;\n      margin-block-start: .5em;\n    }\n\n    li {\n      line-height: 1.2em;\n      margin-block-start: .5em;\n    }\n  }\n\n  // naked/invisible embed (legendary-group)\n  --embed-border-left: none;\n  --embed-border-right: none;\n  --embed-padding: 0;\n\n  .markdown-embed {\n    .markdown-embed-title,\n    .mod-header {\n      display: none;\n    }\n    pre.frontmatter,\n    h1[data-heading] {\n      display: none;\n    }\n    .markdown-embed-content {\n      max-height: unset;\n      overflow: unset;\n  \n      > .markdown-preview-view  {\n        // no scroll\n        overflow-y: unset;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/scss/dnd5e-compendium.scss",
    "content": "/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/dnd5e-compendium.scss */\n@use 'dnd5e/admonitions' as *;\n@use 'dnd5e/statblock' as *;\n@use 'dnd5e/float-images' as *;\n@use 'dnd5e/no-inline-title' as *;\n\n// Change background in Initiative Tracker creature view\n// trim margin on included statblock admonitions\n.creature-view-container.workspace-leaf-content {\n  background-color: var(--background-primary-alt);\n\n  .admonition {\n    margin-top: 0;\n  }\n}\n\n.json5e-index {\n  @media (min-width: 600px) {\n    ul {\n      columns: 2;\n    }\n  }\n}\n"
  },
  {
    "path": "src/scss/dnd5e-float-images.scss",
    "content": "/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/dnd5e-float-images.scss */\n@use 'dnd5e/float-images';\n"
  },
  {
    "path": "src/scss/dnd5e-only-admonitions.scss",
    "content": "/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/dnd5e-only-admonitions.scss */\n@use 'dnd5e/admonitions';\n"
  },
  {
    "path": "src/scss/dnd5e-only-statblock.scss",
    "content": "/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/dnd5e-only-statblock.scss */\n@use 'dnd5e/statblock';\n\n.callout[data-callout=\"statblock\"] {\n  div,\n  img {\n    &[src$=\"#token\"] {\n      float: right;\n      padding-left: 5px;\n      width: 150px;\n    }\n  }\n}\n"
  },
  {
    "path": "src/scss/hide-markdown-link-url.scss",
    "content": "/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/hide-markdown-link-url.scss */\n/* Collapse external links / markdown links in edit mode */\ndiv.cm-line:not(.cm-active) {\n  > .cm-string.cm-url:not(.cm-formatting) {\n    font-size: 0;\n  }\n  > .cm-string.cm-url:not(.cm-formatting)::after {\n    content: '\\27B9';\n    font-size: .8rem;\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2-compendium.scss",
    "content": "/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-compendium.scss */\n// Variables\n@use 'pf2e/pf2e-variables' as var;\n\n// Styling\n@use 'pf2e/styling/index' as style;\n@use 'pf2e/callmonitions/00-callmonitions-index' as callmonitions;\n\n// Patches\n@use 'pf2e/patches/index' as patches;\n"
  },
  {
    "path": "src/scss/pf2-only-statblocks.scss",
    "content": "/*! Source: https://github.com/ebullient/ttrpg-convert-cli/blob/main/src/scss/pf2-only-statblocks.scss */\n@use 'pf2e/callmonitions/_04-statblock-pf2e.scss' as *;\n\n@mixin statblock-link-styles {\n  background-clip: border-box;\n  border-radius: 0.3125rem;\n  border-style: solid;\n  color: rgb(232, 232, 232) !important;\n  cursor: help;\n  font-weight: 700;\n  padding: 0.0625rem;\n  margin: 0.125rem 0.125rem 0.75rem;\n  text-align: center;\n  text-indent: 0;\n  word-break: keep-all;\n  font-style: normal;\n  display: inline-block;\n\n  li & {\n    margin-bottom: 0;\n  }\n}\n\n@mixin statblock-callout-actions {\n  content: \"\";\n  width: 1em;\n  height: 1em;\n  color: transparent;\n}\n\nbody .callout[data-callout=\"statblock-pf2e\"] {\n\n  &[title=\"Common Rarity Trait\"] {\n    background: rgb(232, 232, 232);\n    border-color: rgb(232, 232, 232);\n\n    background-clip: border-box;\n    border-radius: 0.3125rem;\n    border-style: solid;\n    color: rgb(0, 0, 0) !important;\n    cursor: help;\n    font-weight: 700;\n    padding: 0.0625rem;\n    margin: 0.125rem;\n    text-align: center;\n    text-indent: 0;\n    word-break: keep-all;\n    font-style: normal;\n    display: inline-block;\n\n    border-width: 0.0625rem;\n\n    li & {\n      margin-bottom: 0;\n    }\n  }\n\n  &[title=\"Uncommon Rarity Trait\"] {\n    background: rgb(152, 81, 61);\n    border-color: rgb(152, 81, 61);\n\n    @include statblock-link-styles;\n  }\n\n  &[title=\"Rare Rarity Trait\"] {\n    background: rgb(0, 38, 100);\n    border-color: rgb(0, 38, 100);\n\n    @include statblock-link-styles;\n  }\n\n  &[title=\"Unique Rarity Trait\"] {\n    background: rgb(84, 22, 110);\n    border-color: rgb(84, 22, 110);\n\n    @include statblock-link-styles;\n  }\n\n  // Alignment\n\n  &[title*=\"Alignment Trait\"] {\n    background: rgb(102, 111, 153);\n    border-color: rgb(102, 111, 153);\n\n    @include statblock-link-styles;\n  }\n\n  // Sizing\n\n  &[title*=\"Size Trait\"] {\n    background: rgb(82, 122, 95);\n    border-color: rgb(82, 122, 95);\n\n    @include statblock-link-styles;\n  }\n\n  // Other Traits\n  &[title*=\"Action & Ability Trait\"],\n  &[title*=\"Ancestry & Heritage Trait\"],\n  &[title*=\"Armor Trait\"],\n  &[title*=\"Class Trait\"],\n  &[title*=\"Combat Trait\"],\n  &[title*=\"Creature Trait\"],\n  &[title*=\"Creature Type Trait\"],\n  &[title*=\"Effect Trait\"],\n  &[title*=\"Element Trait\"],\n  &[title*=\"Equipment Trait\"],\n  &[title*=\"Feat Trait\"],\n  &[title*=\"General Trait\"],\n  &[title*=\"Gravity Trait\"],\n  &[title*=\"Hazard Trait\"],\n  &[title*=\"Item Trait\"],\n  &[title*=\"Morphic Trait\"],\n  &[title*=\"Planar Trait\"],\n  &[title*=\"Settlement Trait\"],\n  &[title*=\"School Trait\"],\n  &[title*=\"Spell Trait\"],\n  &[title*=\"Tradition Trait\"],\n  &[title*=\"Weapon Trait\"] {\n    background: rgb(97, 20, 5);\n    border-color: rgb(97, 20, 5);\n\n    @include statblock-link-styles;\n\n  }\n\n  & a.internal-link[href$=\"#Actions\"][title=\"Single Action\"] {\n    background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 2040 1024'%3E%3Cpath d='M1022 512L510 0 263 247l265 265-265 265 247 247 512-512zM185 329L2 512l183 183 183-183-183-183z'/%3E%3C/svg%3E\");\n    background-size: cover;\n    background-repeat: no-repeat;\n    display: inline-block;\n    vertical-align: top;\n\n    @include statblock-callout-actions;\n  }\n\n  & a.internal-link[href$=\"#Actions\"][title=\"Two-Action\"] {\n    background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 2040 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1018 512L507 1 261 248l264 264-264 264 246 247 511-511zM183 329L0 512l183 183 183-183-183-183zm1417 175L1152 56 935 272l232 232-232 232 217 217 448-449z'/%3E%3C/svg%3E\");\n    background-repeat: no-repeat;\n    background-size: 1.5625rem;\n    line-height: 1.5625rem;\n    vertical-align: top;\n\n    @include statblock-callout-actions;\n  }\n\n  & a.internal-link[href$=\"#Actions\"][title=\"Three-Action\"] {\n    background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 2128 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m1025 512-512-512-247 247 265 265-265 265 247 247zm1098-10-360-360-175 174 187 186-187 187 175 174zm-1935-173-183 183 183 183 183-183zm1427 175-449-449-218 217 233 232-233 233 218 217z'/%3E%3C/svg%3E\");\n    background-repeat: no-repeat;\n    line-height: 1.5625rem;\n    vertical-align: top;\n\n    @include statblock-callout-actions;\n  }\n\n  & a.internal-link[href$=\"#Actions\"][title=\"Reaction\"] {\n    background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 2128 1024'%3E%3Cpath d='M1013 377q-23-71-82-128-60-57-143.5-94T603 105q-101-14-207 1-75 11-140 35t-116 58q-51 34-87 77-36 42-53 89 21-26 50-48 29-23 64.5-41t76.5-31q42-13 88-20 85-12 166-1t148.5 40.5Q661 294 709 340t66 103q18 58 2 113-17 55-60.5 100.5T605 735q-67 32-152 44-28 4-55.5 5.5t-54.5.5q63 14 131.5 17t137.5-7q106-15 190-56 84-40 138-96.5t75-125.5q20-68-2-140zM152 823l489 103-109-149 56-215-436 261z'/%3E%3C/svg%3E\");\n    background-size: cover;\n    display: inline-block;\n    vertical-align: middle;\n\n    @include statblock-callout-actions;\n  }\n\n  & a.internal-link[href$=\"#Actions\"][title=\"Free Action\"] {\n    background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 2128 1024'%3E%3Cpath d='M516 0L267 250 0 518l509 506 515-515L516 0zM188 496l109-109 109 109-109 109-109-109zm311 380L386 763l251-251-245-246 124-124 359 359-376 375z'/%3E%3C/svg%3E\");\n    background-size: cover;\n    display: inline-block;\n    vertical-align: top;\n\n    @include statblock-callout-actions;\n  }\n\n  & a.internal-link[href$=\"#Actions\"][title=\"Varies\"] {\n    background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpath d='M16 2c-7.732 0-14 6.268-14 14s6.268 14 14 14 14-6.268 14-14-6.268-14-14-14zM28.998 15.952c-3.808 1.889-8.19 1.875-11.971 0 3.625-2.404 5.958-6.273 6.398-10.608 3.352 2.343 5.557 6.22 5.573 10.608zM22.46 4.735c-0.273 4.234-2.484 8.022-5.989 10.353-0.268-4.347-2.447-8.296-5.983-10.845 1.677-0.789 3.54-1.243 5.512-1.243 2.352 0 4.555 0.638 6.46 1.735zM9.471 4.776c3.532 2.354 5.708 6.151 5.974 10.359-3.9-1.941-8.413-2.028-12.389-0.238 0.365-4.322 2.853-8.040 6.415-10.121zM3.002 16.047c3.808-1.887 8.192-1.873 11.972 0.001-3.628 2.405-5.96 6.27-6.4 10.607-3.352-2.343-5.556-6.22-5.572-10.608zM9.542 27.266c0.273-4.236 2.478-8.021 5.987-10.354 0.268 4.352 2.447 8.295 5.984 10.844-1.676 0.79-3.54 1.244-5.513 1.244-2.352 0-4.553-0.637-6.458-1.734zM22.531 27.222c-3.534-2.354-5.711-6.145-5.976-10.359 2.035 1.013 4.237 1.523 6.442 1.523 2.025 0 4.045-0.455 5.949-1.314-0.357 4.334-2.845 8.064-6.415 10.15z'%3E%3C/path%3E%3C/svg%3E\");\n    background-size: cover;\n    display: inline-block;\n    vertical-align: top;\n\n    @include statblock-callout-actions;\n  }\n\n  & a.internal-link[href$=\"#Actions\"][title=\"Duration or Frequency\"] {\n    background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' version='1.1' width='20' height='20' viewBox='0 0 20 20'%3E%3Ctitle%3Ehour-glass%3C/title%3E%3Cpath d='M15.6 4.576c0-2.139 0-2.348 0-2.348 0-0.789-2.508-2.228-5.6-2.228-3.093 0-5.6 1.439-5.6 2.228 0 0 0 0.209 0 2.348 0 2.141 3.877 3.908 3.877 5.424 0 1.514-3.877 3.281-3.877 5.422s0 2.35 0 2.35c0 0.788 2.507 2.228 5.6 2.228 3.092 0 5.6-1.44 5.6-2.229 0 0 0-0.209 0-2.35s-3.877-3.908-3.877-5.422c0-1.515 3.877-3.282 3.877-5.423zM5.941 2.328c0.696-0.439 2-1.082 4.114-1.082s4.006 1.082 4.006 1.082c0.142 0.086 0.698 0.383 0.317 0.609-0.838 0.497-2.478 1.020-4.378 1.020s-3.484-0.576-4.324-1.074c-0.381-0.225 0.265-0.555 0.265-0.555zM10.501 10c0 1.193 0.996 1.961 2.051 2.986 0.771 0.748 1.826 1.773 1.826 2.435v1.328c-0.97-0.483-3.872-0.955-3.872-2.504 0-0.783-1.013-0.783-1.013 0 0 1.549-2.902 2.021-3.872 2.504v-1.328c0-0.662 1.056-1.688 1.826-2.435 1.055-1.025 2.051-1.793 2.051-2.986s-0.996-1.961-2.051-2.986c-0.771-0.75-1.826-1.775-1.826-2.438l-0.046-0.998c1.026 0.553 2.652 1.078 4.425 1.078 1.772 0 3.406-0.525 4.433-1.078l-0.055 0.998c0 0.662-1.056 1.688-1.826 2.438-1.054 1.025-2.051 1.793-2.051 2.986z'%3E%3C/path%3E%3C/svg%3E%0A\");\n    background-size: cover;\n    display: inline-block;\n    vertical-align: top;\n\n    @include statblock-callout-actions;\n  }\n\n}\n"
  },
  {
    "path": "src/scss/pf2e/_pf2e-variables.scss",
    "content": "// Specific Colors to Track\n$box-condition: rgb(19, 138, 6);\n$box-skills: rgb(169, 108, 116);\n$box-traits: rgb(97, 20, 5);\n\n// Blacks\n$almost-black: rgb(36, 36, 36);\n\n// Browns\n$dark-grayish-orange: rgb(160, 148, 127);\n\n// Greys\n$very-dark-grey: rgb(51, 51, 51);\n$very-light-grey: rgb(232, 232, 232);\n\n\n// Greens\n$dark-lime-green: rgb(19, 138, 6);\n$strong-green: rgb(154, 205, 50);\n$very-desaturated-dark-cyan-green: rgb(42, 72, 62);\n\n// Yellow\n$light-grayish-yellow: rgb(234, 229, 213);\n$light-yellow: rgb(255, 219, 88);\n$very-soft-yellow: rgb(217, 204, 140);\n\n\n// Blues\n$dark-moderate-blue: rgb(80, 62, 141);\n$light-moderate-blue: rgb(64, 105, 157);\n$moderate-blue: rgb(116, 90, 205);\n$very-dark-blue: rgb(6, 20, 53);\n$very-dark-teal: rgb(0, 73, 97);\n$very-desaturated-blue: rgb(44, 70, 78);\n$very-desaturated-dark-blue: rgb(51, 66, 82);\n\n// Purples\n$dark-violet: rgb(107, 0, 153);\n$tyrian-purple: rgb(100, 2, 59);\n$very-light-violet: rgb(222, 145, 255);\n\n\n// Pinks\n\n\n// Red\n$dark-grayish-magenta: rgb(80, 67, 79);\n$dark-red: rgb(128, 0, 0);\n$very-desaturated-red: rgb(102, 51, 51);\n$very-light-red: rgb(255, 143, 143);\n\n\n// Orange\n$strong-orange: rgb(204, 72, 0);\n\n\n@import url('https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap');\n\n@font-face {\n  font-family: \"Pathfinder\";\n  src: url(\"data:application/x-font-ttf;charset=utf-8;base64,AAEAAAANAIAAAwBQRkZUTaBf/YkAAAkkAAAAHEdERUYAJQAAAAAJDAAAABhPUy8yDzE5wAAAAVgAAABgY21hcIUPLFwAAAHcAAABcGdhc3AAAAAQAAAJBAAAAAhnbHlmv0sljgAAA2AAAAIwaGVhZCgbkG4AAADcAAAANmhoZWEMNQhqAAABFAAAACRobXR4IOUAhwAAAbgAAAAkbG9jYQGoAlQAAANMAAAAFG1heHAADgA/AAABOAAAACBuYW1l5gKtSAAABZAAAAMGcG9zdJP9aoAAAAiYAAAAbAABAAAAARmaJrDlgl8PPPUACwQAAAAAAN/r3gMAAAAA4J1UVAAA/8AIcwPAAAAACAACAAAAAAAAAAEAAAPA/8AAAAigAAAAAAhzAAEAAAAAAAAAAAAAAAAAAAAJAAEAAAAJAD0ABAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAwVCAZAABQAAApkCzAAAAI8CmQLMAAAB6wAzAQkAAAAAAAAAAAAAAAAAAAABAAAAIAAAAAAAAAAAAAAAAABAAAArUwPA/8AAQAPAAEAAAAABAAAAAAAAAAAAAAAgAAEEAAAAAAAAAAFVAAAAAAAABFAAKAaAACAEAAACCKAALQQgABAAAAADAAAAAwAAABwAAQAAAAAAagADAAEAAAAcAAQATgAAAAwACAACAAQAASsyKz0rU//9//8AAAAAKzIrOitT//3//wAA1NIAANS1AAAAAQAMAAAADAAAABAAAAABAAMABQAGAAAABwAAAAABBgAAAQMAAAAAAAABAgAAAAIAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAIABAAeACgALwA8AEYAAEAAAAAAAAAAAACAAA5AgABAAAAAAAAAAAAAgAAOQIAAgAoACIEMwNlADcAPAAAASYnJicmJyYHBgcGBwYHBgc2NzY3Njc2NzYXFhcWFxYXFgcGBwYHBgcGBwYnFhcWNzY3Njc2NzYBBSc3AQQdFzs8VFNlZWpLQUEzMyQkERUdHSMkKSouVVFRQ0QwMBISEBEsK0RDVRwcGxs/REVFalRUNjYVFPyNAeltOP5MAkdHOTklJQ0ODwsYGCIiKyovGhYXEhINDQcMCwsdHi4uOTo3Ny0uISAMBAECAQ4DAwoPKSg5OEVE/opnldf++wAAAAMAIP/BBmADvwAFAAkADwAACQEHCQEXAQcXNyUBBxcHFwQa/gH2AQj++Pb+vLe3twTS/kDZ6OjZAcAB//f++P749wK2t7e3CAHA2Ojo2QAAAgAC/8AD/gPAAAUACQAACQEHCQEXAQcXNwP+/gD3AQn+9/f+u7e3twHAAgD3/vf+9/cCt7e3twAEAC3/wAhzA8AABQALAA8AFQAACQEHCQEXCQEHFwcXAQcXNyUBBxcHFwQp/gD3AQn+9/cGSv6Yr7u7r/nZt7e3BNz+P9rp6doBwAIA9/73/vf3AgoBaK66u64CFre3twgBwdno6dkAAAMAEP/ABBADwAAEAAkADwAACQU3FwcnASc3JzcBAhT9/AH9AgP+BP64bW1tbQE3cfv1fAFnA8D9+v4GAgMB/f4QbW1tbf6Ecfv2fP6ZAAAAAAASAN4AAQAAAAAAAQAKABYAAQAAAAAAAgAHADEAAQAAAAAAAwAKAE8AAQAAAAAABAAKAHAAAQAAAAAABQALAJMAAQAAAAAABgAKALUAAQAAAAAACgA2AS4AAQAAAAAADQAXAZUAAQAAAAAADgAoAf8AAwABBAkAAQAUAAAAAwABBAkAAgAOACEAAwABBAkAAwAUADkAAwABBAkABAAUAFoAAwABBAkABQAWAHsAAwABBAkABgAUAJ8AAwABBAkACgBsAMAAAwABBAkADQAuAWUAAwABBAkADgBQAa0AUABhAHQAaABmAGkAbgBkAGUAcgAAUGF0aGZpbmRlcgAAUgBlAGcAdQBsAGEAcgAAUmVndWxhcgAAUABhAHQAaABmAGkAbgBkAGUAcgAAUGF0aGZpbmRlcgAAUABhAHQAaABmAGkAbgBkAGUAcgAAUGF0aGZpbmRlcgAAVgBlAHIAcwBpAG8AbgAgADEALgAxAABWZXJzaW9uIDEuMQAAUABhAHQAaABmAGkAbgBkAGUAcgAAUGF0aGZpbmRlcgAAUABhAHQAaABmAGkAbgBkAGUAcgAgADIAZQAgAEEAYwB0AGkAbwBuACAARwBsAHkAcABoAHMACgBGAG8AbgB0ACAAZwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABJAGMAbwBNAG8AbwBuAC4AAFBhdGhmaW5kZXIgMmUgQWN0aW9uIEdseXBocwpGb250IGdlbmVyYXRlZCBieSBJY29Nb29uLgAAUABhAGkAegBvACAAQwBvAG0AbQB1AG4AaQB0AHkAIABMAGkAYwBlAG4AcwBlAABQYWl6byBDb21tdW5pdHkgTGljZW5zZQAAaAB0AHQAcABzADoALwAvAHAAYQBpAHoAbwAuAGMAbwBtAC8AYwBvAG0AbQB1AG4AaQB0AHkALwBjAG8AbQBtAHUAbgBpAHQAeQB1AHMAZQAAaHR0cHM6Ly9wYWl6by5jb20vY29tbXVuaXR5L2NvbW11bml0eXVzZQAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkAAAECAAIBAwEEAQUBBgEHAQgHdW5pMDAwMAd1bmkwMDAxB3VuaTJCMzIHdW5pMkIzQQd1bmkyQjNCB3VuaTJCM0QHdW5pMkI1MwABAAH//wAPAAEAAAAMAAAAEAAAAAIAAAAEAAAAAgAAAAAAAQAAAADf1ssxAAAAAN/r3gMAAAAA4J1UVA==\") format(\"truetype\");\n  font-weight: normal;\n  font-style: normal;\n  font-display: block;\n  unicode-range: U+2B32, U+2B3A, U+2B3B, U+2B3D, U+2B53;\n}\n\nbody {\n  // Font Families\n  --pf2e-font-monospace-default: Menlo, SFMono-Regular, Consolas, 'Roboto Mono', 'Source Code Pro', monospace;\n  --pf2e-font-monospace-theme: 'Fira Code', Menlo, Monaco, 'Courier New', 'Source Code Pro', Jetbrains Mono;\n  --pf2e-font-monospace: var(--pf2e-font-monospace-theme), var(--pf2e-font-monospace-default);\n  --pf2e-font-headers-default: 'Nunito', sans-serif;\n  --pf2e-font-headers-theme: 'Poppins', 'Montserrat', 'Roboto', 'Open Sans', 'Helvetica Neue', sans-serif;\n  --pf2e-font-headers: var(--pf2e-font-headers-default), var(--pf2e-font-headers-theme);\n\n\n  // On Pathfinder Brand\n  --pf2e-black-text-rgb: 38, 38, 38;\n  --pf2e-black-text: #262626;\n  --pf2e-blue-headers-rgb: 11, 37, 96;\n  --pf2e-blue-headers: #0b2560;\n  --pf2e-red-headers-rgb: 85, 12, 6;\n  --pf2e-red-headers: #550c06;\n  --pf2e-light-brown-headers: #9f6a57;\n  --pf2e-light-brown-headers-rgb: 159, 106, 87;\n\n  // Tables\n  --pf2e-beige-table-even-rows-rgb: 244, 239, 226;\n  --pf2e-beige-table-even-rows: #f4efe2;\n  --pf2e-tan-table-odd-rows-rgb: 236, 228, 203;\n  --pf2e-tan-table-odd-rows: #ece4cb;\n  --pf2e-brown-table-borders: #9c7566;\n  --pf2e-brown-table-borders-rgb: 156, 117, 102;\n\n  // Header Specifics\n  --pf2e-header-one: rgba(209, 30, 50, 1);\n  --pf2e-header-two: rgba(72, 128, 255, 1);\n  --pf2e-header-three: rgba(209, 30, 50, 1);\n  --pf2e-header-four: rgba(226, 151, 124, 1);\n\n  // Scroll Bars\n  --scrollbar-active-thumb-bg: rgba(204, 204, 204, 1);\n  --scrollbar-hover: rgba(51, 51, 51, 1);\n  --scrollbar-thumb-bg: rgba(102, 102, 102, 1);\n\n  // Links\n  --pf2e-purple-link: rgb(222, 145, 255);\n  --pf2e-blue-link: rgb(64, 105, 157);\n  --pf2e-green-link: rgb(154, 205, 50);\n}\n\n.theme-light {\n  // On Pathfinder Brand\n  --pf2e-black-text-rgb: 38, 38, 38;\n  --pf2e-black-text: #262626;\n  --pf2e-blue-headers-rgb: 11, 37, 96;\n  --pf2e-blue-headers: #0b2560;\n  --pf2e-red-headers-rgb: 85, 12, 6;\n  --pf2e-red-headers: #550c06;\n  --pf2e-light-brown-headers: #764f41;\n  --pf2e-light-brown-headers-rgb: 118, 79, 65;\n\n  // Tables\n  --pf2e-beige-table-even-rows-rgb: 244, 239, 226;\n  --pf2e-beige-table-even-rows: #f4efe2;\n  --pf2e-tan-table-odd-rows-rgb: 236, 228, 203;\n  --pf2e-tan-table-odd-rows: #ece4cb;\n  --pf2e-brown-table-borders: var(--pf2e-light-brown-headers);\n  --pf2e-brown-table-borders-rgb: rgb(var(--pf2e-light-brown-headers-rgb));\n  --pf2e-table-hover-color: 241, 167, 162;\n\n  // Scroll Bars\n  --scrollbar-active-thumb-bg: rgba(102, 102, 102, 1);\n  --scrollbar-hover: rgba(204, 204, 204, 1);\n  --scrollbar-thumb-bg: rgba(153, 153, 153, 1);\n\n  // Links\n  --pf2e-purple-link: rgb(107, 0, 153);\n  --pf2e-blue-link: rgb(0, 73, 97);\n  --pf2e-green-link: rgb(19, 138, 6);\n}\n"
  },
  {
    "path": "src/scss/pf2e/callmonitions/_00-callmonitions-index.scss",
    "content": "// Variables\n@use '../pf2e-variables' as var;\n\n// Forwards for Callouts\n@forward '01-callmonition-core';\n@forward '02-pf2-beige';\n@forward '02-pf2-brown';\n@forward '02-pf2-example';\n@forward '02-pf2-inset';\n@forward '02-pf2-key-box';\n@forward '02-pf2-note';\n@forward '02-pf2-red';\n@forward '02-pf2-sidebar';\n@forward '02-pf2-tip';\n@forward '02-success-degree';\n\n// Forwards for Admonitions\n@forward '03-ad-pf2-note';\n@forward '03-embed-ability';\n@forward '03-embed-action';\n@forward '03-embed-avatar';\n@forward '03-embed-disease';\n@forward '03-embed-feat';\n@forward '03-embed-item';\n@forward '03-embed-ritual';\n@forward '03-inline-affliction';\n@forward '03-inline-attack';\n@forward '02-pf2-summary';\n@forward '04-statblock-pf2e';\n"
  },
  {
    "path": "src/scss/pf2e/callmonitions/_00-pf2e-maps.scss",
    "content": "//Maps\n$pf2e-callouts: (\n        pf2-beige: #f0e9d6,\n        pf2-brown: #d2beaa,\n        pf2-example: #ccdbbd,\n        pf2-inset: #e9e4d9,\n        pf2-key-box: #550c06,\n        pf2-quote: #e7aad4,\n        pf2-sidebar: #f4f3f0,\n        pf2-red: #550c06,\n        pf2-tip: #d1d3d4,\n        pf2-note: #e3dfbb,\n        success-degree: #e6e6b4,\n);\n\n$pf2e-admonitions: (\n        pf2-note: rgb(227, 223, 187), // Rage of the Elements\n  // The above appear as a callout and as an admonition\n        embed-ability: rgb(182, 197, 200), // Players Guide\n        embed-action: rgb(238, 221, 227), // Bestiary 1\n        embed-avatar: rgb(236, 202, 99), // Bestiary 2\n        embed-disease: rgb(36, 36, 36), // Bestiary 3\n        embed-feat: rgb(239, 187, 169), // Core Rulebook\n        embed-item: rgb(163, 200, 209), // Dark Archive\n        embed-ritual: rgb(158, 187, 144), // Book of Dead\n        inline-affliction: rgb(218, 216, 218), // Game-mastery Guide\n        inline-attack: rgb(245, 229, 188), //Guns and Gears\n        pf2-summary: rgb(209, 224, 224), // Secrets of Magic\n        statblock-pf2: rgb(239, 215, 143), // Pathfinder Logo\n);\n"
  },
  {
    "path": "src/scss/pf2e/callmonitions/_01-callmonition-core.scss",
    "content": "// Variables\n@use '../pf2e-variables' as var;\n\n//Maps\n@use '00-pf2e-maps' as callout-map;\n@use '00-pf2e-maps' as admonition-map;\n\n// Mixins Next\n@use 'shake-ins/ca-root' as root;\n@use 'shake-ins/ca-title' as title;\n@use 'shake-ins/ca-content' as body;\n@use 'shake-ins/ca-misc' as extras;\n\n\n// Specifics...\n// Local Variables\n$call-box-shadow: rgba(26, 34, 77, 0.9);\n$svg-size-content: 1rem;\n$svg-size: 1.125rem;\n\n// Callout Mass Production\n@each $key, $value in callout-map.$pf2e-callouts {\n  .callout[data-callout='#{$key}'] {\n    --callout-border-opacity: 0.3;\n    --callout-color: #{$value};\n    // Mixins Here\n    @include root.common-root;\n    @include root.callout-drop;\n\n    // General Styling Here\n    background-color: #{$value};\n    color: var.$almost-black;\n    border-color: var(--pf2e-light-brown-headers);\n    border-style: solid;\n    border-width: 0.05rem;\n    margin: 0.1875rem 0.125rem 0.125rem;\n    padding: 0.125rem;\n\n    & .callout-title {\n\n      // Mixins Here\n      @include title.title;\n\n      // General Styling Here\n\n      & .callout-icon .svg-icon {\n        display: none;\n      }\n\n      & .callout-title-inner {\n\n        // Mixins Here\n        @include title.inner-top;\n\n        // General Styling Here\n        font-weight: 700;\n        margin-left: 1rem;\n        padding-bottom: 0.125rem;\n      }\n    }\n\n\n    & .callout-content {\n      background-color: #{$value};\n      padding: 1rem;\n\n      // Mixins Here\n      @include body.content-headings(1);\n      @include body.content-headings(2);\n      @include body.content-headings(3);\n      @include body.content-code;\n      @include body.content-links;\n      @include body.content-strong;\n\n      & blockquote {\n        @include body.content-blockquotes;\n      }\n\n      @include extras.callout-extras-p-bottom;\n\n\n      // General Styling Here\n\n    }\n  }\n}\n\n//Time to color this Seed\n@each $key, $value in admonition-map.$pf2e-admonitions {\n  .admonition[data-callout='#{$key}'] {\n    --callout-border-opacity: 0.3;\n    --callout-color: #{$value};\n    --callout-content-background: #{$value};\n    // Mixins Here\n    @include root.common-root;\n\n    // General Styling Here\n    box-shadow: 0 0 5px 0 $call-box-shadow;\n    border-color: var(--pf2e-light-brown-headers);\n    border-style: solid;\n    border-width: 0.05rem;\n    color: var.$almost-black;\n    margin: 3px 0.125rem 0.125rem;\n    padding: 0.125rem;\n\n    & .admonition-title {\n      background-color: #{$value};\n      // Mixins Here\n      @include title.title;\n\n      // General Styling Here\n\n      & .admonition-title-icon .svg-icon {\n\n        // General Styling Here\n        display: block;\n        height: 1.5rem;\n        left: 0.5rem;\n        margin-left: 0.5rem;\n        position: absolute;\n        top: 0.5rem;\n        width: 1.5rem;\n\n        &:has(.content) {\n          height: $svg-size-content;\n          width: $svg-size-content;\n        }\n      }\n\n      & .admonition-title-inner {\n        // Mixins Here\n        @include title.inner-top;\n\n        // General Styling Here\n        font-weight: 700;\n        padding-bottom: 0.125rem;\n        margin-right: 0.125rem;\n      }\n    }\n\n\n    & .admonition-content {\n      background-color: #{$value};\n      padding: 1rem;\n      // Mixins Here\n      @include body.content-headings(1);\n      @include body.content-headings(2);\n      @include body.content-headings(3);\n      @include body.content-code;\n      @include body.content-links;\n      @include body.content-strong;\n\n      & blockquote {\n        @include body.content-blockquotes;\n      }\n\n      //General Styling Here\n      @include extras.callout-extras-p-bottom;\n      @include body.admonition-callout-kids;\n    }\n\n    @include body.admonition-copy;\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2e/callmonitions/_02-pf2-beige.scss",
    "content": "// Variables\n@use '../pf2e-variables' as var;\n@use 'shake-ins/admon-content-styling' as content-styles;\n\n$beige-padding-25: 0.25rem;\n\n.callout[data-callout='pf2-beige'] {\n\n  & .callout-title {\n    & .callout-title-inner {\n      margin-left: $beige-padding-25;\n      padding-bottom: $beige-padding-25;\n    }\n  }\n\n  & .callout-content {\n    @include content-styles.common-admonition-typography;\n\n    h1, h2, h3, h4, h5, h6 {\n      text-align: left;\n      padding-left: $beige-padding-25;\n    }\n  }\n\n}\n"
  },
  {
    "path": "src/scss/pf2e/callmonitions/_02-pf2-brown.scss",
    "content": "// Variables\n@use '../pf2e-variables' as var;\n@use 'shake-ins/admon-content-styling' as content-styles;\n\n.callout[data-callout='pf2-brown'] {\n\n  & .callout-title {\n    & .callout-title-inner {\n      font-family: 'Helvetica Neue', Helvetica, Arial, 'Pathfinder', sans-serif;\n      margin-left: 0.25rem;\n      text-align: center;\n    }\n  }\n\n  & .callout-content {\n    @include content-styles.common-admonition-typography;\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2e/callmonitions/_02-pf2-example.scss",
    "content": "// Variables\n@use '../pf2e-variables' as var;\n@use 'shake-ins/admon-content-styling' as content-styles;\n\n.callout.callout[data-callout='pf2-example'] {\n\n  & .callout-content {\n    @include content-styles.common-admonition-typography;\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2e/callmonitions/_02-pf2-inset.scss",
    "content": "// Variables\n@use '../pf2e-variables' as var;\n\n.callout[data-callout='pf2-inset'] {\n\n  & .callout-title {\n    & .callout-title-inner {\n      display: none;\n    }\n  }\n\n  & .callout-content {\n    font-weight: 700;\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2e/callmonitions/_02-pf2-key-box.scss",
    "content": "// Variables\n@use '../pf2e-variables' as var;\n@use 'shake-ins/admon-content-styling' as content-styles;\n\n.callout[data-callout='pf2-key-box'] {\n  color: var.$very-light-grey;\n  clear: left;\n  display: inline-flex;\n  float-wrap: wrap;\n  float: left;\n  font-family: 'Times New Roman', Times, serif;\n  text-transform: capitalize;\n\n  & .callout-content {\n    @include content-styles.embed-disease-content;\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2e/callmonitions/_02-pf2-note.scss",
    "content": "// Variables\n@use '../pf2e-variables' as var;\n@use 'shake-ins/admon-content-styling' as content-styles;\n\n.callout[data-callout=pf2-note] {\n  clear: both;\n  font-style: italic;\n  text-align: center;\n  margin-bottom: 1rem;\n\n  & .callout-title {\n\n    & .callout-title-inner {\n      flex: unset;\n      display: none;\n\n      .admonition-parent & {\n        display: inline;\n      }\n    }\n  }\n\n  & .callout-content {\n    @include content-styles.common-admonition-typography;\n    margin-bottom: -0.5rem;\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2e/callmonitions/_02-pf2-red.scss",
    "content": "// Variables\n@use '../pf2e-variables' as var;\n@use 'shake-ins/admon-content-styling' as content-styles;\n.callout[data-callout='pf2-red'] {\n  color: var.$very-light-grey;\n\n  & .callout-title {\n\n    & .callout-title-inner {\n      font-family: 'Times New Roman', Times, serif;\n\n      strong {\n        color: var.$very-light-red;\n      }\n\n      a {\n        color: var.$strong-orange;\n      }\n    }\n  }\n\n  & .callout-content {\n    @include content-styles.embed-disease-content;\n\n    table {\n      & th {\n        color: var.$very-light-grey;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2e/callmonitions/_02-pf2-sidebar.scss",
    "content": "// Variables\n@use '../pf2e-variables' as var;\n@use 'shake-ins/admon-content-styling' as content-styles;\n\n.callout[data-callout='pf2-sidebar'] {\n  color: var.$dark-red;\n  clear: right;\n  float: right;\n  max-height: 85vh;\n  max-width: 50%;\n  overflow-wrap: break-word;\n  overflow-y: scroll;\n\n  & .callout-content {\n    @include content-styles.common-admonition-typography;\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2e/callmonitions/_02-pf2-summary.scss",
    "content": "// Variables\n@use '../pf2e-variables' as var;\n@use 'shake-ins/admon-content-styling' as content-styles;\n\n.admonition[data-callout='pf2-summary'] {\n  --callout-icon: '<svg xmlns=\"http://www.w3.org/2000/svg\"   viewBox=\"0 0 1024 1024\"><path fill=\"currentColor\" d=\"M854.173 764.089H712.398l-1.355-2.419c.501-86.681 34.198-175.368 98.214-245.312 47.831-52.261 83.95-121.041 102.458-189.368l-82.338-49.795 76.414-13.791-86.599-51.529 104.084-17.674c-10.445-78.138-57.588-136.002-154.489-136.002-94.565 18.374-363.959 23.983-503.322 5.922-17.664-5.751-35.459-9.136-52.817-9.731-.129-.035-.267-.068-.394-.102l.219.097c-45.31-1.502-87.613 16.03-116.708 60.236-40.414 61.408 2.616 166.812 97.48 166.812 5.359 0 10.253-.289 14.727-.832v.733h118.467c-17.921 72.21-62.36 152.533-113.828 233.474a564.359 564.359 0 0 0-12.575 20.767l69.657 71.359-112.351 18.818c-65.349 171.651-54.745 363.552 96.403 363.552v-.002l15.634.11v.234h171.175v-.311h451.103c135.99.002 154.656-225.247-37.485-225.247zM452.277 872.408c-.003-.935-.03-1.888-.055-2.836.025.95.052 1.903.055 2.836zm-4.386-35.788c.027.119.047.232.074.351-.027-.117-.047-.231-.074-.347-5.374-23.696-20.779-43.454-40.897-56.175h.007c20.114 12.721 35.516 32.477 40.89 56.171zm4.214 29.647zm-.459-6.386zm-.765-6.682c-.125-.924-.242-1.844-.384-2.781.144.937.261 1.858.384 2.781zm-1.113-7.143c-.144-.814-.269-1.615-.426-2.437.157.822.282 1.624.426 2.437zM236.579 269.87c28.031-23.871-1.146-80.303-34.722-127.243h115.747c19.346 37.343 21.335 80.462 11.471 127.243h-92.496zm89.202 522.253c51.786-17.213 110.468 15.682 122.11 67.013 1.991 8.782 3.269 17.061 3.911 24.882.075-.817.112-1.662.17-2.491-1.291 18.519-6.792 33.936-15.602 46.701h-1.034a76.702 76.702 0 0 1-3.062 4.103l1.614-.707c-14.802 19.359-37.539 32.29-64.57 40.636h-28.104l1.368-.6c-29.614-.008-63.612-2.798-63.612-2.798 43.925-.703 62.862-19.294 70.065-40.598-31.298 11.376-64.654-9.072-75.146-39.264-14.161-40.748 12.502-83.784 51.893-96.877zm126.458 83.452c-.018.919-.027 1.846-.065 2.75.038-.905.045-1.831.065-2.75zm-18.22 55.872z\"></path></svg>';\n\n  & .admonition-content {\n  @include content-styles.common-admonition-typography;\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2e/callmonitions/_02-pf2-tip.scss",
    "content": "// Variables\n@use '../pf2e-variables' as var;\n@use 'shake-ins/admon-content-styling' as content-styles;\n\n.callout[data-callout='pf2-tip'] {\n  color: var.$almost-black;\n\n   em {\n    color: mix(var.$very-soft-yellow, var.$very-light-grey, 50%);\n  }\n\n  strong {\n    color: mix(var.$very-desaturated-dark-blue, var.$very-light-grey, 50%);\n  }\n\n  strong em {\n    color: mix(var.$very-dark-grey, var.$very-desaturated-dark-blue, 50%);\n  }\n\n  & .callout-title {\n    background-color: var.$very-dark-blue;\n\n    & .callout-title-inner {\n      color: var.$very-soft-yellow;\n      margin-left: 0.25rem;\n      text-align: left;\n\n      a {\n        color: lighten(var.$very-desaturated-red, 20);\n      }\n    }\n  }\n\n  & .callout-content {\n    @include content-styles.pft-tip-content;\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2e/callmonitions/_02-success-degree.scss",
    "content": "// Variables\n@use '../pf2e-variables' as var;\n@use 'shake-ins/admon-content-styling' as content-styles;\n\n$margins: auto;\n\n.callout[data-callout='success-degree'] {\n  --callout-icon: lucide-scale;\n\n  & .callout-title {\n    display: inherit;\n\n    & .callout-icon .svg-icon {\n      display: block;\n      margin-bottom: $margins;\n      margin-left: $margins;\n      margin-right: $margins;\n    }\n\n    & .callout-title-inner {\n      display: none;\n    }\n  }\n\n  & .callout-content {\n    @include content-styles.common-admonition-typography;\n    strong {\n      font-weight: 800;\n    }\n\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2e/callmonitions/_03-ad-pf2-note.scss",
    "content": "// Variables\n@use '../pf2e-variables' as var;\n@use 'shake-ins/admon-content-styling' as content-styles;\n\n.admonition.admonition[data-callout='pf2-note'] {\n  --callout-icon: '<svg xmlns=\"http://www.w3.org/2000/svg\"   viewBox=\"0 0 1024 1024\"><path fill=\"currentColor\" d=\"M854.173 764.089H712.398l-1.355-2.419c.501-86.681 34.198-175.368 98.214-245.312 47.831-52.261 83.95-121.041 102.458-189.368l-82.338-49.795 76.414-13.791-86.599-51.529 104.084-17.674c-10.445-78.138-57.588-136.002-154.489-136.002-94.565 18.374-363.959 23.983-503.322 5.922-17.664-5.751-35.459-9.136-52.817-9.731-.129-.035-.267-.068-.394-.102l.219.097c-45.31-1.502-87.613 16.03-116.708 60.236-40.414 61.408 2.616 166.812 97.48 166.812 5.359 0 10.253-.289 14.727-.832v.733h118.467c-17.921 72.21-62.36 152.533-113.828 233.474a564.359 564.359 0 0 0-12.575 20.767l69.657 71.359-112.351 18.818c-65.349 171.651-54.745 363.552 96.403 363.552v-.002l15.634.11v.234h171.175v-.311h451.103c135.99.002 154.656-225.247-37.485-225.247zM452.277 872.408c-.003-.935-.03-1.888-.055-2.836.025.95.052 1.903.055 2.836zm-4.386-35.788c.027.119.047.232.074.351-.027-.117-.047-.231-.074-.347-5.374-23.696-20.779-43.454-40.897-56.175h.007c20.114 12.721 35.516 32.477 40.89 56.171zm4.214 29.647zm-.459-6.386zm-.765-6.682c-.125-.924-.242-1.844-.384-2.781.144.937.261 1.858.384 2.781zm-1.113-7.143c-.144-.814-.269-1.615-.426-2.437.157.822.282 1.624.426 2.437zM236.579 269.87c28.031-23.871-1.146-80.303-34.722-127.243h115.747c19.346 37.343 21.335 80.462 11.471 127.243h-92.496zm89.202 522.253c51.786-17.213 110.468 15.682 122.11 67.013 1.991 8.782 3.269 17.061 3.911 24.882.075-.817.112-1.662.17-2.491-1.291 18.519-6.792 33.936-15.602 46.701h-1.034a76.702 76.702 0 0 1-3.062 4.103l1.614-.707c-14.802 19.359-37.539 32.29-64.57 40.636h-28.104l1.368-.6c-29.614-.008-63.612-2.798-63.612-2.798 43.925-.703 62.862-19.294 70.065-40.598-31.298 11.376-64.654-9.072-75.146-39.264-14.161-40.748 12.502-83.784 51.893-96.877zm126.458 83.452c-.018.919-.027 1.846-.065 2.75.038-.905.045-1.831.065-2.75zm-18.22 55.872z\"></path></svg>';\n\n  & .admonition-title {\n\n    & .admonition-title-inner {\n      display: inline;\n    }\n  }\n\n  & .admonition-content {\n@include content-styles.common-admonition-typography;\n\n    ul, ol, li {\n      text-align: left;\n    }\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2e/callmonitions/_03-embed-ability.scss",
    "content": "// Variables\n@use '../pf2e-variables' as var;\n@use 'shake-ins/admon-content-styling' as content-styles;\n\n.admonition[data-callout='embed-ability'] {\n  --callout-icon: '<svg xmlns=\"http://www.w3.org/2000/svg\"   viewBox=\"0 0 1024 1024\"><path fill=\"currentColor\" d=\"M908.682 797.44c139.17-191.525 122.422-461.107-50.286-633.818-176.21-176.21-453.266-190.09-645.372-41.652-27.817-32.566-54.723-65.058-78.801-95.832L30.954 129.413c30.778 23.828 63.451 50.248 96.266 77.522-151.767 192.241-138.96 471.937 38.483 649.38 172.129 172.129 440.475 189.331 631.878 51.674l40.78 40.778 154.422 38.458-43.596-149.283-40.504-40.502zM660.181 548.939L426.963 315.724l87.314-87.321-63.667-63.672c180.697-9.014 330.398 207.332 209.571 384.207zM446.375 164.971L342.652 268.702a11470.78 11470.78 0 0 1-39.017-43.372c46.748-38.523 95.723-57.103 142.74-60.359zm-212.31 133.097a6682.191 6682.191 0 0 1 42.134 37.09L168.442 442.922l65.666 65.67 82.032-82.037L794.61 905.02C412.147 1162.465-75.257 688.289 234.064 298.069z\"></path></svg>';\n  \n  & .admonition-content {\n    @include content-styles.common-admonition-typography;\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2e/callmonitions/_03-embed-action.scss",
    "content": "// Variables\n@use '../pf2e-variables' as var;\n@use 'shake-ins/admon-content-styling' as content-styles;\n\n.admonition[data-callout='embed-action'] {\n  --callout-icon: '<svg aria-hidden=\"true\" focusable=\"false\" data-prefix=\"fas\" data-icon=\"campground\" class=\"svg-inline--fa fa-campground fa-w-20\" role=\"img\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 640 512\"><path fill=\"currentColor\" d=\"M624 448h-24.68L359.54 117.75l53.41-73.55c5.19-7.15 3.61-17.16-3.54-22.35l-25.9-18.79c-7.15-5.19-17.15-3.61-22.35 3.55L320 63.3 278.83 6.6c-5.19-7.15-15.2-8.74-22.35-3.55l-25.88 18.8c-7.15 5.19-8.74 15.2-3.54 22.35l53.41 73.55L40.68 448H16c-8.84 0-16 7.16-16 16v32c0 8.84 7.16 16 16 16h608c8.84 0 16-7.16 16-16v-32c0-8.84-7.16-16-16-16zM320 288l116.36 160H203.64L320 288z\"></path></svg>';\n\n  & .admonition-content {\n    @include content-styles.common-admonition-typography;\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2e/callmonitions/_03-embed-avatar.scss",
    "content": "// Variables\n@use '../pf2e-variables' as var;\n@use 'shake-ins/admon-content-styling' as content-styles;\n\n.admonition[data-callout='embed-avatar'] {\n  --callout-icon: '<svg aria-hidden=\"true\" focusable=\"false\" data-prefix=\"fas\" data-icon=\"church\" class=\"svg-inline--fa fa-church fa-w-20\" role=\"img\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 640 512\"><path fill=\"currentColor\" d=\"M464.46 246.68L352 179.2V128h48c8.84 0 16-7.16 16-16V80c0-8.84-7.16-16-16-16h-48V16c0-8.84-7.16-16-16-16h-32c-8.84 0-16 7.16-16 16v48h-48c-8.84 0-16 7.16-16 16v32c0 8.84 7.16 16 16 16h48v51.2l-112.46 67.48A31.997 31.997 0 0 0 160 274.12V512h96v-96c0-35.35 28.65-64 64-64s64 28.65 64 64v96h96V274.12c0-11.24-5.9-21.66-15.54-27.44zM0 395.96V496c0 8.84 7.16 16 16 16h112V320L19.39 366.54A32.024 32.024 0 0 0 0 395.96zm620.61-29.42L512 320v192h112c8.84 0 16-7.16 16-16V395.96c0-12.8-7.63-24.37-19.39-29.42z\"></path></svg>';\n\n  a.internal-link {\n    color: var.$tyrian-purple;\n\n    &:hover {\n      color: darken(var.$tyrian-purple, 10%);\n    }\n\n    &:is([href*='#Actions']) {\n      color: transparent;\n    }\n  }\n\n  & .admonition-title {\n\n    & .admonition-title-content {\n      font-family: Garamond, 'Baskerville Old Face', 'Hoefler Text', 'Times New Roman', serif;\n      text-align: center;\n    }\n\n  }\n  \n  & .admonition-content {\n    @include content-styles.embed-avatar-content;\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2e/callmonitions/_03-embed-disease.scss",
    "content": "// Variables\n@use '../pf2e-variables' as var;\n@use 'shake-ins/admon-content-styling' as content-styles;\n\n.admonition[data-callout='embed-disease'] {\n  --callout-icon: '<svg aria-hidden=\"true\" focusable=\"false\" data-prefix=\"fas\" data-icon=\"head-side-virus\" class=\"svg-inline--fa fa-head-side-virus fa-w-16\" role=\"img\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\"><path fill=\"currentColor\" d=\"M272,240a16,16,0,1,0,16,16A16,16,0,0,0,272,240Zm-64-64a16,16,0,1,0,16,16A16,16,0,0,0,208,176Zm301.2,99c-20.93-47.12-48.43-151.73-73.07-186.75A207.9,207.9,0,0,0,266.09,0H192C86,0,0,86,0,192A191.23,191.23,0,0,0,64,334.81V512H320V448h64a64,64,0,0,0,64-64V320H480A32,32,0,0,0,509.2,275ZM368,240H355.88c-28.51,0-42.79,34.47-22.63,54.63l8.58,8.57a16,16,0,1,1-22.63,22.63l-8.57-8.58C290.47,297.09,256,311.37,256,339.88V352a16,16,0,0,1-32,0V339.88c0-28.51-34.47-42.79-54.63-22.63l-8.57,8.58a16,16,0,0,1-22.63-22.63l8.58-8.57c20.16-20.16,5.88-54.63-22.63-54.63H112a16,16,0,0,1,0-32h12.12c28.51,0,42.79-34.47,22.63-54.63l-8.58-8.57a16,16,0,0,1,22.63-22.63l8.57,8.58c20.16,20.16,54.63,5.88,54.63-22.63V96a16,16,0,0,1,32,0v12.12c0,28.51,34.47,42.79,54.63,22.63l8.57-8.58a16,16,0,0,1,22.63,22.63l-8.58,8.57C313.09,173.53,327.37,208,355.88,208H368a16,16,0,0,1,0,32Z\"></path></svg>';\n\n  color: var.$very-light-grey;\n  font-family: 'Abril Fatface', 'Cinzel Decorative', 'Creepster', 'Henny Penny', 'IM Fell English', 'Kaushan Script', 'Lobster', 'Montserrat Alternates', 'Nosifer', 'Permanent Marker', 'Playfair Display', 'Righteous', 'Roboto Slab', 'Special Elite', 'UnifrakturCook', cursive;\n\n  & .admonition-content {\n    @include content-styles.embed-disease-content;\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2e/callmonitions/_03-embed-feat.scss",
    "content": "// Variables\n@use '../pf2e-variables' as var;\n@use 'shake-ins/admon-content-styling' as content-styles;\n\n.admonition[data-callout='embed-feat'] {\n  --callout-icon: '<svg aria-hidden=\"true\" focusable=\"false\" data-prefix=\"fas\" data-icon=\"award\" class=\"svg-inline--fa fa-award fa-w-12\" role=\"img\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 384 512\"><path fill=\"currentColor\" d=\"M97.12 362.63c-8.69-8.69-4.16-6.24-25.12-11.85-9.51-2.55-17.87-7.45-25.43-13.32L1.2 448.7c-4.39 10.77 3.81 22.47 15.43 22.03l52.69-2.01L105.56 507c8 8.44 22.04 5.81 26.43-4.96l52.05-127.62c-10.84 6.04-22.87 9.58-35.31 9.58-19.5 0-37.82-7.59-51.61-21.37zM382.8 448.7l-45.37-111.24c-7.56 5.88-15.92 10.77-25.43 13.32-21.07 5.64-16.45 3.18-25.12 11.85-13.79 13.78-32.12 21.37-51.62 21.37-12.44 0-24.47-3.55-35.31-9.58L252 502.04c4.39 10.77 18.44 13.4 26.43 4.96l36.25-38.28 52.69 2.01c11.62.44 19.82-11.27 15.43-22.03zM263 340c15.28-15.55 17.03-14.21 38.79-20.14 13.89-3.79 24.75-14.84 28.47-28.98 7.48-28.4 5.54-24.97 25.95-45.75 10.17-10.35 14.14-25.44 10.42-39.58-7.47-28.38-7.48-24.42 0-52.83 3.72-14.14-.25-29.23-10.42-39.58-20.41-20.78-18.47-17.36-25.95-45.75-3.72-14.14-14.58-25.19-28.47-28.98-27.88-7.61-24.52-5.62-44.95-26.41-10.17-10.35-25-14.4-38.89-10.61-27.87 7.6-23.98 7.61-51.9 0-13.89-3.79-28.72.25-38.89 10.61-20.41 20.78-17.05 18.8-44.94 26.41-13.89 3.79-24.75 14.84-28.47 28.98-7.47 28.39-5.54 24.97-25.95 45.75-10.17 10.35-14.15 25.44-10.42 39.58 7.47 28.36 7.48 24.4 0 52.82-3.72 14.14.25 29.23 10.42 39.59 20.41 20.78 18.47 17.35 25.95 45.75 3.72 14.14 14.58 25.19 28.47 28.98C104.6 325.96 106.27 325 121 340c13.23 13.47 33.84 15.88 49.74 5.82a39.676 39.676 0 0 1 42.53 0c15.89 10.06 36.5 7.65 49.73-5.82zM97.66 175.96c0-53.03 42.24-96.02 94.34-96.02s94.34 42.99 94.34 96.02-42.24 96.02-94.34 96.02-94.34-42.99-94.34-96.02z\"></path></svg>';\n\n  & .admonition-content {\n    @include content-styles.common-admonition-typography;\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2e/callmonitions/_03-embed-item.scss",
    "content": "// Variables\n@use '../pf2e-variables' as var;\n@use 'shake-ins/admon-content-styling' as content-styles;\n\n.admonition[data-callout='embed-item'] {\n  --callout-icon: '<svg aria-hidden=\"true\" focusable=\"false\" data-prefix=\"fas\" data-icon=\"coins\" class=\"svg-inline--fa fa-coins fa-w-16\" role=\"img\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\"><path fill=\"currentColor\" d=\"M0 405.3V448c0 35.3 86 64 192 64s192-28.7 192-64v-42.7C342.7 434.4 267.2 448 192 448S41.3 434.4 0 405.3zM320 128c106 0 192-28.7 192-64S426 0 320 0 128 28.7 128 64s86 64 192 64zM0 300.4V352c0 35.3 86 64 192 64s192-28.7 192-64v-51.6c-41.3 34-116.9 51.6-192 51.6S41.3 334.4 0 300.4zm416 11c57.3-11.1 96-31.7 96-55.4v-42.7c-23.2 16.4-57.3 27.6-96 34.5v63.6zM192 160C86 160 0 195.8 0 240s86 80 192 80 192-35.8 192-80-86-80-192-80zm219.3 56.3c60-10.8 100.7-32 100.7-56.3v-42.7c-35.5 25.1-96.5 38.6-160.7 41.8 29.5 14.3 51.2 33.5 60 57.2z\"></path></svg>';\n\n  & .admonition-content {\n    @include content-styles.common-admonition-typography;\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2e/callmonitions/_03-embed-ritual.scss",
    "content": "// Variables\n@use '../pf2e-variables' as var;\n@use 'shake-ins/admon-content-styling' as content-styles;\n\n.admonition[data-callout='embed-ritual'] {\n  --callout-icon: '<svg aria-hidden=\"true\" focusable=\"false\" data-prefix=\"fas\" data-icon=\"pray\" class=\"svg-inline--fa fa-pray fa-w-12\" role=\"img\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 384 512\"><path fill=\"currentColor\" d=\"M256 128c35.35 0 64-28.65 64-64S291.35 0 256 0s-64 28.65-64 64 28.65 64 64 64zm-30.63 169.75c14.06 16.72 39 19.09 55.97 5.22l88-72.02c17.09-13.98 19.59-39.19 5.62-56.28-13.97-17.11-39.19-19.59-56.31-5.62l-57.44 47-38.91-46.31c-15.44-18.39-39.22-27.92-64-25.33-24.19 2.48-45.25 16.27-56.37 36.92l-49.37 92.03c-23.4 43.64-8.69 96.37 34.19 123.75L131.56 432H40c-22.09 0-40 17.91-40 40s17.91 40 40 40h208c34.08 0 53.77-42.79 28.28-68.28L166.42 333.86l34.8-64.87 24.15 28.76z\"></path></svg>';\n\n  & .admonition-content {\n    @include content-styles.common-admonition-typography;\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2e/callmonitions/_03-inline-affliction.scss",
    "content": "// Variables\n@use '../pf2e-variables' as var;\n@use 'shake-ins/admon-content-styling' as content-styles;\n\n.admonition[data-callout='inline-affliction'] {\n  --callout-icon: '<svg aria-hidden=\"true\" focusable=\"false\" data-prefix=\"fas\" data-icon=\"virus-slash\" class=\"svg-inline--fa fa-virus-slash fa-w-20\" role=\"img\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 640 512\"><path fill=\"currentColor\" d=\"M114,227.6H92.4C76.7,227.6,64,240.3,64,256s12.7,28.4,28.4,28.4H114c50.7,0,76.1,61.3,40.2,97.1L139,396.8 c-11.5,10.7-12.2,28.7-1.6,40.2s28.7,12.2,40.2,1.6c0.5-0.5,1.1-1,1.6-1.6l15.2-15.2c35.8-35.8,97.1-10.5,97.1,40.2v21.5 c0,15.7,12.8,28.4,28.5,28.4c15.7,0,28.4-12.7,28.4-28.4V462c0-26.6,17-45.9,38.2-53.4l-244.5-189 C133.7,224.7,123.9,227.5,114,227.6z M617,505.8l19.6-25.3c5.4-7,4.2-17-2.8-22.5L470.6,332c4.2-25.4,24.9-47.5,55.4-47.5h21.5 c15.7,0,28.4-12.7,28.4-28.4s-12.7-28.4-28.4-28.4H526c-50.7,0-76.1-61.3-40.2-97.1l15.2-15.3c10.7-11.5,10-29.5-1.6-40.2 c-10.9-10.1-27.7-10.1-38.6,0l-15.2,15.2c-35.8,35.8-97.1,10.5-97.1-40.2V28.5C348.4,12.7,335.7,0,320,0 c-15.7,0-28.4,12.7-28.4,28.4V50c0,50.7-61.3,76.1-97.1,40.2L179.2,75c-11.1-11.1-29.4-10.6-40.5,0.5L45.5,3.4 c-7-5.4-17-4.2-22.5,2.8L3.4,31.5c-5.4,7-4.2,17,2.8,22.5l588.4,454.7C601.5,514.1,611.6,512.8,617,505.8z M335.4,227.5l-62.9-48.6 c4.9-1.8,10.2-2.8,15.4-2.9c26.5,0,48,21.5,48,48C336,225.2,335.5,226.3,335.4,227.5z\"></path></svg>';\n\n  & .admonition-title-content {\n    color: var.$dark-grayish-magenta\n  }\n\n\n  & .admonition-content {\n    @include content-styles.common-admonition-typography;\n  }\n\n}\n"
  },
  {
    "path": "src/scss/pf2e/callmonitions/_03-inline-attack.scss",
    "content": "// Variables\n@use '../pf2e-variables' as var;\n@use 'shake-ins/admon-content-styling' as content-styles;\n\n\n.admonition[data-callout='inline-attack'] {\n  --callout-icon: '<svg xmlns=\"http://www.w3.org/2000/svg\"   viewBox=\"0 0 1024 1024\"><path fill=\"currentColor\" d=\"M746.013 346.338c46.494-110.134 130.621 19.244 105.629 112.518-19.444 72.579-84.175 117.611-176.313 58.351l.002-.003c-211.764-136.2-156.573-202.882-263.845-314.147 36.755 155.341-72.25 56.868-170.627-68.533 28.348-16.96 60.092-28.884 94.211-34.921l220.269 148.443 41.1-86.039a516.489 516.489 0 0 1 17.294 11.65C733.437 257.936 719.977 58.832 756.047 17.728l-735.76-1.517 3.623 719.657c78.285 54.695 209.606-14.959 156.548-102.913a642.424 642.424 0 0 1-10.895-18.781l60.214-35.715-123.371-219.527c2.806-33.01 10.277-63.857 21.748-91.975 143.591 147.62 367.783 380.048 166.483 334.739 61.101 57.174 274.614 99.542 298.674 195.863 19.974 79.966-50.973 143.517-111.372 143.517-81.1 0-165.35-64.925-89.556-140.074-48.131 8.002-71.065 41.941-72.331 80.371s19.341 81.427 58.557 107.924h605.069V339.45c-14.114-118.729-241.943-168.205-237.665 6.887zM20.288 16.21v-.005z\"></path></svg>';\n\n  & .admonition-content {\n    @include content-styles.common-admonition-typography;\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2e/callmonitions/_04-statblock-pf2e.scss",
    "content": "// Imports\n@import \"https://fonts.googleapis.com/css2?family=Oswald:wght@700&display=swap\";\n\n// Mixins\n\n@mixin statblock-link-styles {\n  border-radius: 5px;\n  color: rgb(232, 232, 232) !important;\n  cursor: help;\n  font-weight: 700;\n  margin-bottom: 0.75rem;\n  padding: 0.1rem;\n  text-align: center;\n  text-indent: 0;\n  word-break: keep-all;\n  font-style: normal;\n  display: inline-block;\n}\n\n// Callout Core\n\nbody {\n  .callout[data-callout=\"statblock-pf2e\"] {\n    background-color: rgb(246, 244, 242);\n    border: none;\n\n    --callout-color: var(--statblock-pf2e);\n\n    color: rgb(0, 0, 0);\n    font-family: sans-serif;\n    font-size: 16px;\n    font-weight: 400;\n    line-height: 1.3em;\n    margin: 1rem 0;\n    min-width: 50%;\n    mix-blend-mode: normal;\n    padding: 0 0.25em;\n\n    // Theme Dark\n    .theme-dark & {\n      --statblock-pf2e: 201, 60, 60;\n    }\n\n    // Theme Light\n\n    .theme-light & {\n      --statblock-pf2e: 201, 60, 60;\n    }\n\n    // Strong + Em\n\n    strong {\n      color: rgb(0, 0, 0);\n    }\n\n    em {\n      color: rgb(0, 0, 0);\n    }\n\n    strong + em {\n      color: rgb(0, 0, 0);\n    }\n\n    // Callout Title Box\n\n    & .callout-title {\n      background-color: rgb(246, 244, 242);\n      border: none;\n      border-radius: 0;\n      color: rgb(0, 0, 0);\n      display: flex;\n      font-family: \"Oswald\", sans-serif;\n      font-size: inherit;\n      gap: 0;\n      line-height: 1.3;\n      margin-bottom: 0;\n      padding: 0.25em 0 0;\n\n      & .callout-icon {\n        display: none;\n      }\n\n      & .callout-title-inner {\n        color: rgb(0, 0, 0);\n        flex: 1;\n        font-size: 1.35em;\n        font-weight: 700;\n        line-height: 1;\n        margin-bottom: 0;\n        margin-left: 0.25em;\n        padding-bottom: 0;\n        position: relative;\n        text-align: left;\n        text-transform: uppercase;\n      }\n    }\n\n    // Make Images with an Anchor of #Token Float Right\n\n    img,\n    div {\n      &[src$=\"#token\"] {\n        float: right;\n        margin-left: 0.3125em;\n        width: 9.375em;\n      }\n    }\n\n    // Main Callout Content\n\n    & .callout-content {\n      background-color: rgb(246, 244, 242);\n      margin-top: 0;\n      padding-left: 0.25em;\n      padding-right: 0.25em;\n      padding-top: 0;\n\n      // Link Styling\n\n      & a {\n        color: rgb(51, 122, 183);\n        font-weight: 700;\n        text-decoration: none;\n      }\n\n      & a.external-link {\n        background-image: none;\n        background-size: 0;\n        color: rgb(150, 122, 222);\n        padding-right: 0;\n\n        &::after {\n          display: none;\n        }\n      }\n\n      & .internal-link.is-unresolved::after {\n        display: none;\n      }\n\n      // Fix Blockquote Looks\n\n      & blockquote {\n        background-color: rgb(246, 244, 242);\n        border: none;\n        color: rgb(0, 0, 0);\n        margin-inline-end: 1em;\n        margin-inline-start: 2em;\n        padding: 0;\n      }\n\n      // Fix P Spacing\n\n      > p {\n        margin-block-start: .5em;\n      }\n\n      // Fix LI Spacing\n\n      & li {\n        line-height: 1.2em;\n        margin-block-start: .5em;\n      }\n\n      // Make HR thinner and remove common Decorations\n\n      > hr {\n        border-color: rgb(0, 0, 0);\n        border-top: 1px solid;\n        height: 1px;\n        margin: 0;\n        width: 100%;\n\n        &::before {\n          display: none;\n        }\n\n        &::after {\n          display: none;\n        }\n\n        &:has(.admonition):has(.is-live-preview) {\n          margin-block-start: 0.5em;\n        }\n      }\n    }\n  }\n\n  // Admonitions Only\n\n  .admonition-statblock-pf2e-parent {\n    & .admonition-content {\n      > p {\n        margin-block-end: 0.25em;\n        margin-block-start: 0.5em;\n      }\n    }\n  }\n}\n\n// Span Tag for Creature Float\n\n.creature {\n  float: right;\n  margin-right: 0.5em;\n}\n\n// Span Tag for Sourcebook Float\n\n.sourcebook {\n  float: right;\n  margin-bottom: 0.5em;\n  margin-right: 0.5em;\n}\n\n// Specific Fix for Subnested callouts using ITS theme metadata\n\nbody .callout[data-callout=\"statblock-pf2e\"] .callout[data-callout-metadata~=\"no-title\"] > .callout-title {\n  display: none;\n}\n\n// Specific Fix for Widths\n\nbody .markdown-reading-view .callout[data-callout=\"statblock-pf2e\"] {\n  width: 70%;\n}\n\nbody .markdown-reading-view .callout[data-callout=\"statblock-pf2e\"]:has(.creature-statblock-container) {\n  width: 100%;\n}\n\n\nbody .is-live-preview .callout[data-callout=\"statblock-pf2e\"] {\n  width: 70%;\n}\n\nbody .is-live-preview .callout[data-callout=\"statblock-pf2e\"]:has(.creature-statblock-container) {\n  width: 100%;\n}\n\n.published-container .callout[data-callout=\"statblock-pf2e\"] {\n  max-width: 70%;\n  min-width: 40%;\n}\n"
  },
  {
    "path": "src/scss/pf2e/callmonitions/shake-ins/_admon-content-styling.scss",
    "content": "// Variables\n@use '../../pf2e-variables' as var;\n\n@mixin common-admonition-typography {\n\n  a.internal-link {\n    color: var.$very-desaturated-red;\n\n    &:hover {\n      color: lighten(var.$very-desaturated-red, 20);\n    }\n\n    &.is-unresolved {\n      color: var.$very-dark-teal;\n\n      &:hover {\n        color: lighten(var.$very-dark-teal, 20%);\n      }\n    }\n\n    &:is([href*='#Actions']) {\n      color: transparent;\n\n      &:has(.admonition) {\n        vertical-align: sub;\n      }\n    }\n  }\n\n  a.external-link {\n    color: var.$dark-violet;\n\n\n    &:hover {\n      color: lighten(var.$dark-violet, 20%);\n    }\n  }\n\n  em {\n    color: var.$very-dark-grey;\n  }\n\n  h1 {\n    color: var.$dark-grayish-magenta;\n  }\n  h2 {\n    color: var.$very-desaturated-blue;\n  }\n  h3 {\n    color: var.$very-dark-grey;\n  }\n  h4 {\n    color: var.$very-desaturated-dark-cyan-green;\n  }\n  h5 {\n    color: var.$very-desaturated-red;\n  }\n  h6 {\n    color: var.$almost-black;\n  }\n\n  strong {\n    color: var.$very-desaturated-dark-blue;\n  }\n\n  strong em {\n    color: mix(var.$very-dark-grey, var.$very-desaturated-dark-blue, 50%);\n  }\n\n  ol > li::marker,\n  ul > li::marker {\n    color: var.$strong-orange;\n  }\n}\n\n@mixin embed-avatar-content {\n\n  a.internal-link {\n    &.is-unresolved {\n      color: var.$very-dark-teal; // rgba(0, 72, 96, 1);\n\n\n      &:hover {\n        color: lighten(var.$very-dark-teal, 20%);\n\n      }\n    }\n  }\n\n  a.external-link {\n    color: var.$dark-violet; //rgba(107, 0, 153, 1);\n\n\n    &:hover {\n      color: darken(var.$dark-violet, 20%);\n    }\n  }\n\n  em {\n    color: var.$very-dark-grey;\n  }\n\n  h1 {\n    color: var.$very-desaturated-red;\n  }\n\n  h2 {\n    color: var.$very-desaturated-blue;\n  }\n\n  h3 {\n    color: var.$dark-grayish-magenta;\n  }\n\n  h4 {\n    color: var.$very-desaturated-dark-cyan-green;\n  }\n\n  h5 {\n    color: var.$very-dark-grey;\n  }\n\n  h6 {\n    color: var.$almost-black;\n  }\n\n  strong {\n    color: var.$very-desaturated-dark-blue;\n  }\n\n  strong em {\n    color: mix(var.$very-dark-grey, var.$very-desaturated-dark-blue, 50%);\n  }\n\n  ol > li::marker,\n  ul > li::marker {\n    color: var.$dark-lime-green;\n  }\n\n}\n\n@mixin embed-disease-content {\n\n  a.internal-link {\n    color: var.$light-yellow;\n\n    &:hover {\n      color: darken(var.$light-yellow, 20);\n    }\n\n    &.is-unresolved {\n      color: mix(var.$light-yellow, var.$strong-green, 25%);\n\n\n      &:hover {\n        color: darken(mix(var.$light-yellow, var.$strong-green, 25%), 20%);\n\n      }\n    }\n  }\n\n  a.external-link {\n    color: rgb(var(--callout-color));\n    filter: invert(100%);\n\n    &:hover {\n      filter: invert(100%) brightness(1.2);\n    }\n  }\n\n  em {\n    color: var.$strong-green;\n  }\n\n  h1, h2, h3, h4, h5, h6 {\n    color: var.$very-light-grey;\n  }\n\n  p {\n    color: var.$very-light-grey;\n  }\n\n  strong {\n    color: var.$very-light-red;\n  }\n\n  strong em {\n    color: mix(var.$strong-green, var.$very-light-red, 50%);\n  }\n\n  ol > li::marker,\n  ul > li::marker {\n    color: var.$strong-orange;\n  }\n\n  ul {\n    margin-block-end: unset;\n    margin-block-start: unset;\n    padding: 0.50em;\n  }\n}\n\n\n@mixin pft-tip-content {\n  a.internal-link {\n    color: var.$very-desaturated-red;\n\n\n    &:hover {\n      color: lighten(var.$very-desaturated-red, 20);\n    }\n\n    &.is-unresolved {\n      color: var.$very-dark-teal;\n\n\n      &:hover {\n        color: lighten(var.$very-dark-teal, 20%);\n\n      }\n    }\n  }\n\n  a.external-link {\n    color: var.$dark-violet;\n\n\n    &:hover {\n      color: lighten(var.$dark-violet, 20%);\n    }\n  }\n\n  em {\n    color: var.$very-dark-grey;\n  }\n\n  h1 {\n    color: var.$dark-grayish-magenta;\n  }\n  h2 {\n    color: var.$very-desaturated-blue;\n  }\n  h3 {\n    color: var.$very-dark-grey;\n  }\n  h4 {\n    color: var.$very-desaturated-dark-cyan-green;\n  }\n  h5 {\n    color: var.$very-desaturated-red;\n  }\n  h6 {\n    color: var.$almost-black;\n  }\n\n  strong {\n    color: var.$very-desaturated-dark-blue;\n  }\n\n  strong em {\n    color: mix(var.$very-dark-grey, var.$very-desaturated-dark-blue, 50%);\n  }\n\n  ol > li::marker,\n  ul > li::marker {\n    color: var.$strong-orange;\n  }\n\n  p {\n    text-align: justify;\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2e/callmonitions/shake-ins/_ca-content.scss",
    "content": "// Variables\n@use '../../pf2e-variables' as var;\n\n$h1-size: clamp(1.125rem, 1.5em, 1.375rem);\n$h2-size: clamp(1rem, 1.3125em, 1.25rem);\n$h3-size: clamp(0.875rem, 1.125em, 1rem);\n$h4-size: clamp(0.75rem, 1em, 0.875rem);\n$h5-size: clamp(0.625rem, 0.9375em, 0.75rem);\n$h6-size: clamp(0.8125em, 0.5rem, 0.625rem);\n\n// Common Mixins for Functions\n@mixin content-headings($level) {\n  $sizes: (\n          1: $h1-size,\n          2: $h2-size,\n          3: $h3-size,\n          4: $h4-size,\n          5: $h5-size,\n          6: $h6-size,\n  );\n\n  $padding-bottoms: (0.125em, 0.25em, 0.125em, 0.5em, 0.25em, 0.75em);\n\n  // Functions\n  @for $i from 1 through 6 {\n    & h#{$i} {\n      font-size: map-get($sizes, $i);\n      padding-bottom: nth($padding-bottoms, $i);\n      text-align: center;\n      // Add custom styles for h3 here\n      @if $i == 3 {\n        &::before {\n          display: none;\n        }\n\n        &::after {\n          display: none;\n        }\n      }\n    }\n  }\n}\n\n\n// Common Mixins for Callout and Admonitions\n@mixin content-blockquotes {\n  padding: 0.3125rem 0.3125rem 0.3125rem 0.625rem;\n  border-right: 0.1875rem solid;\n  border-left: 0.1875rem solid;\n  margin-left: 0.75em;\n  margin-right: 0.75em;\n}\n\n// & code {\n@mixin content-code {\n  & code {\n    background-color: var.$very-light-grey;\n    color: var.$dark-moderate-blue;\n    font-family: var(--pf2e-font-monospace);\n  }\n}\n\n// Admonition Only\n@mixin admonition-copy {\n  .admonition-content-copy {\n    opacity: 0;\n    margin: 0.3125rem;\n    right: 0;\n    top: 0;\n    transition: 1s opacity ease-in-out;\n\n    &:hover {\n      color: mix(var.$very-light-grey, transparent, 30%);\n    }\n  }\n}\n\n// Link Fixes\n@mixin content-links {\n\n  a.internal-link {\n    font-weight: 600;\n    text-decoration: none;\n\n\n    &:hover {\n      text-decoration: underline;\n    }\n\n    &.is-unresolved {\n      font-style: italic;\n      text-decoration: none;\n\n      &:hover {\n        text-decoration: underline;\n      }\n    }\n  }\n\n  a.external-link {\n    font-weight: 600;\n    text-decoration: none;\n\n    &:hover {\n      text-decoration: underline;\n    }\n  }\n}\n\n@mixin content-strong {\n\n  strong {\n    padding-inline: 0.25rem;\n  }\n}\n\n@mixin admonition-callout-kids {\n   > .callout {\n     mix-blend-mode: normal;\n     margin-bottom: 1rem;\n\n   }\n}\n"
  },
  {
    "path": "src/scss/pf2e/callmonitions/shake-ins/_ca-misc.scss",
    "content": "// Variables\n@use '../../pf2e-variables' as var;\n\n@mixin callout-extras-p-bottom {\n  & p {\n    line-height: 1.2;\n    margin-block-start: 0;\n    margin-block-end: 0;\n    margin-left: 0.5rem;\n    margin-bottom: 1rem;\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2e/callmonitions/shake-ins/_ca-root.scss",
    "content": "// Variables\n@use '../../pf2e-variables' as var;\n\n// Mixin for common styles\n@mixin root-common {\n  font-size: 1rem;\n  overflow-wrap: break-word;\n  overflow-y: scroll;\n}\n\n// Common styles for .callout and .admonition\n@mixin common-root {\n  --callout-background-alpha: 0.2;\n  --callout-padding: 1rem;\n  --callout-radius: 0.25rem;\n  @include root-common;\n}\n\n// Callout-drop-shadows\n@mixin callout-drop {\n  &:not(.admonition).drop-shadow {\n    box-shadow: var.$very-dark-teal 0 1.25rem 1.875rem -0.625rem;\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2e/callmonitions/shake-ins/_ca-title.scss",
    "content": "// Variables\n@use '../../pf2e-variables' as var;\n\n@mixin title {\n  flex-direction: row;\n  flex-wrap: wrap;\n  justify-content: center;\n  align-items: center;\n  align-content: center;\n\n  border: none;\n  display: flex;\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  position: relative;\n}\n\n\n@mixin inner-top {\n  font-size: clamp(1rem, 1.3125em, 1.25rem);\n\n  em {\n    font-weight: 500;\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2e/patches/_ITS-Theme.scss",
    "content": "// ITS Theme Specific Patches\n\n// Pathfinder Styling\n.pathfinder.pathfinder.pathfinder {\n\n  // Fixes Position of Single Action Icon in Actions Header\n  & h1 a.internal-link[href$=\"#Actions\"][title=\"Single Action\"] {\n    vertical-align: text-bottom;\n  }\n\n  & h1 a.internal-link[href$=\"#Actions\"][title=Two-Action], a.internal-link[href$=\"#Actions\"][title=Two-Action] .callout-title-inner {\n    width: 2.5em;\n    vertical-align: text-bottom;\n  }\n\n  // Fixes coloration when ITS Theme Applies a Bullet to a List\n  & .admonition[data-callout=embed-ability] .admonition-content ol > li::marker, & .admonition[data-callout=embed-ability] .admonition-content ul > li::marker {\n    color: transparent;\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2e/patches/_index.scss",
    "content": "@forward 'ITS-Theme';\n"
  },
  {
    "path": "src/scss/pf2e/styling/_action-icons.scss",
    "content": "// Variables\n@use '../pf2e-variables' as var;\n\n\n// Action icons for Reader and Render Modes\n\n/*! General Settings for #Action & [hrefs]s */\n\n@mixin normal-actions {\n  color: transparent;\n  background-size: cover;\n  background-repeat: no-repeat;\n  display: inline-block;\n\n  h1 & {\n    color: transparent;\n    height: 1.375em;\n    width: 1.375em;\n  }\n\n  &:hover {\n    color: transparent;\n  }\n\n\n  li & {\n    vertical-align: middle;\n  }\n\n}\n\na.internal-link[href$='#Actions'][title='Single Action'] {\n  background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 2128 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1022 512L510 0 263 247l265 265-265 265 247 247 512-512zM185 329L2 512l183 183 183-183-183-183z'/%3E%3C/svg%3E%0A\");\n  @include normal-actions;\n  vertical-align: sub;\n  width: 1em;\n  height: 1em;\n\n  .theme-dark & {\n    background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 2128 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1022 512L510 0 263 247l265 265-265 265 247 247 512-512zM185 329L2 512l183 183 183-183-183-183z' fill='red'/%3E%3C/svg%3E%0A\");\n  }\n\n  h1 & {\n    vertical-align: middle;\n  }\n\n\n}\n\na.internal-link[href$='#Actions'][title='Two-Action'] {\n  background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 2128 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1018 512L507 1 261 248l264 264-264 264 246 247 511-511zM183 329L0 512l183 183 183-183-183-183zm1417 175L1152 56 935 272l232 232-232 232 217 217 448-449z'/%3E%3C/svg%3E\");\n  @include normal-actions;\n  vertical-align: baseline;\n  width: 1.6em;\n  height: 1em;\n\n  h1 &,\n  .callout-title-inner {\n    width: 1.6em;\n  }\n\n  .theme-dark & {\n    background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 2128 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1018 512L507 1 261 248l264 264-264 264 246 247 511-511zM183 329L0 512l183 183 183-183-183-183zm1417 175L1152 56 935 272l232 232-232 232 217 217 448-449z' fill='red'/%3E%3C/svg%3E\");\n  }\n}\n\na.internal-link[href$='#Actions'][title='Three-Action'] {\n  background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 2128 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m1025 512-512-512-247 247 265 265-265 265 247 247zm1098-10-360-360-175 174 187 186-187 187 175 174zm-1935-173-183 183 183 183 183-183zm1427 175-449-449-218 217 233 232-233 233 218 217z'/%3E%3C/svg%3E\");\n  vertical-align: baseline;\n  width: 2.10em;\n  height: 1em;\n\n  @include normal-actions;\n\n  h1 &,\n  .callout-title-inner {\n    width: 2.9em;\n    vertical-align: text-top;\n  }\n\n  .theme-dark & {\n    background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 2128 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m1025 512-512-512-247 247 265 265-265 265 247 247zm1098-10-360-360-175 174 187 186-187 187 175 174zm-1935-173-183 183 183 183 183-183zm1427 175-449-449-218 217 233 232-233 233 218 217z' fill='red' /%3E%3C/svg%3E\");\n  }\n}\n\na.internal-link[href$='#Actions'][title='Reaction'] {\n  background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 2128 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1013 377q-23-71-82-128-60-57-143.5-94T603 105q-101-14-207 1-75 11-140 35t-116 58q-51 34-87 77-36 42-53 89 21-26 50-48 29-23 64.5-41t76.5-31q42-13 88-20 85-12 166-1t148.5 40.5Q661 294 709 340t66 103q18 58 2 113-17 55-60.5 100.5T605 735q-67 32-152 44-28 4-55.5 5.5t-54.5.5q63 14 131.5 17t137.5-7q106-15 190-56 84-40 138-96.5t75-125.5q20-68-2-140zM152 823l489 103-109-149 56-215-436 261z'/%3E%3C/svg%3E\");\n  @include normal-actions;\n  vertical-align: middle;\n  width: 1em;\n  height: 1em;\n\n  h1 &,\n  .callout-title-inner {\n    vertical-align: text-top;\n  }\n\n  .theme-dark & {\n    background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 2128 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1013 377q-23-71-82-128-60-57-143.5-94T603 105q-101-14-207 1-75 11-140 35t-116 58q-51 34-87 77-36 42-53 89 21-26 50-48 29-23 64.5-41t76.5-31q42-13 88-20 85-12 166-1t148.5 40.5Q661 294 709 340t66 103q18 58 2 113-17 55-60.5 100.5T605 735q-67 32-152 44-28 4-55.5 5.5t-54.5.5q63 14 131.5 17t137.5-7q106-15 190-56 84-40 138-96.5t75-125.5q20-68-2-140zM152 823l489 103-109-149 56-215-436 261z' fill='red'/%3E%3C/svg%3E\");\n  }\n}\n\na.internal-link[href$='#Actions'][title='Free Action'] {\n  background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 2128 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M516 0L267 250 0 518l509 506 515-515L516 0zM188 496l109-109 109 109-109 109-109-109zm311 380L386 763l251-251-245-246 124-124 359 359-376 375z'/%3E%3C/svg%3E\");\n  @include normal-actions;\n  vertical-align: baseline;\n  width: 1em;\n  height: 1em;\n\n\n  .theme-dark & {\n    background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 2128 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M516 0L267 250 0 518l509 506 515-515L516 0zM188 496l109-109 109 109-109 109-109-109zm311 380L386 763l251-251-245-246 124-124 359 359-376 375z' fill='red'/%3E%3C/svg%3E\");\n  }\n}\n\na.internal-link[href$='#Actions'][title='Varies'] {\n  background-image: url(\"data:image/svg+xml,%3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Ctitle%3Eload%3C/title%3E%3Cpath d='M16 2c-7.732 0-14 6.268-14 14s6.268 14 14 14 14-6.268 14-14-6.268-14-14-14zM28.998 15.952c-3.808 1.889-8.19 1.875-11.971 0 3.625-2.404 5.958-6.273 6.398-10.608 3.352 2.343 5.557 6.22 5.573 10.608zM22.46 4.735c-0.273 4.234-2.484 8.022-5.989 10.353-0.268-4.347-2.447-8.296-5.983-10.845 1.677-0.789 3.54-1.243 5.512-1.243 2.352 0 4.555 0.638 6.46 1.735zM9.471 4.776c3.532 2.354 5.708 6.151 5.974 10.359-3.9-1.941-8.413-2.028-12.389-0.238 0.365-4.322 2.853-8.040 6.415-10.121zM3.002 16.047c3.808-1.887 8.192-1.873 11.972 0.001-3.628 2.405-5.96 6.27-6.4 10.607-3.352-2.343-5.556-6.22-5.572-10.608zM9.542 27.266c0.273-4.236 2.478-8.021 5.987-10.354 0.268 4.352 2.447 8.295 5.984 10.844-1.676 0.79-3.54 1.244-5.513 1.244-2.352 0-4.553-0.637-6.458-1.734zM22.531 27.222c-3.534-2.354-5.711-6.145-5.976-10.359 2.035 1.013 4.237 1.523 6.442 1.523 2.025 0 4.045-0.455 5.949-1.314-0.357 4.334-2.845 8.064-6.415 10.15z'%3E%3C/path%3E%3C/svg%3E\");\n  @include normal-actions;\n  vertical-align: baseline;\n  width: 1em;\n  height: 1em;\n\n\n  .theme-dark & {\n    background-image: url(\"data:image/svg+xml,%3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Ctitle%3Eload%3C/title%3E%3Cpath d='M16 2c-7.732 0-14 6.268-14 14s6.268 14 14 14 14-6.268 14-14-6.268-14-14-14zM28.998 15.952c-3.808 1.889-8.19 1.875-11.971 0 3.625-2.404 5.958-6.273 6.398-10.608 3.352 2.343 5.557 6.22 5.573 10.608zM22.46 4.735c-0.273 4.234-2.484 8.022-5.989 10.353-0.268-4.347-2.447-8.296-5.983-10.845 1.677-0.789 3.54-1.243 5.512-1.243 2.352 0 4.555 0.638 6.46 1.735zM9.471 4.776c3.532 2.354 5.708 6.151 5.974 10.359-3.9-1.941-8.413-2.028-12.389-0.238 0.365-4.322 2.853-8.040 6.415-10.121zM3.002 16.047c3.808-1.887 8.192-1.873 11.972 0.001-3.628 2.405-5.96 6.27-6.4 10.607-3.352-2.343-5.556-6.22-5.572-10.608zM9.542 27.266c0.273-4.236 2.478-8.021 5.987-10.354 0.268 4.352 2.447 8.295 5.984 10.844-1.676 0.79-3.54 1.244-5.513 1.244-2.352 0-4.553-0.637-6.458-1.734zM22.531 27.222c-3.534-2.354-5.711-6.145-5.976-10.359 2.035 1.013 4.237 1.523 6.442 1.523 2.025 0 4.045-0.455 5.949-1.314-0.357 4.334-2.845 8.064-6.415 10.15z' fill='red'%3E%3C/path%3E%3C/svg%3E%0A\");\n  }\n}\n\na.internal-link[href$='#Actions'][title='Duration or Frequency'] {\n  background-image: url(\"data:image/svg+xml,%3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Ctitle%3Ehour-glass%3C/title%3E%3Cpath d='M15.6 4.576c0-2.139 0-2.348 0-2.348 0-0.789-2.508-2.228-5.6-2.228-3.093 0-5.6 1.439-5.6 2.228 0 0 0 0.209 0 2.348 0 2.141 3.877 3.908 3.877 5.424 0 1.514-3.877 3.281-3.877 5.422s0 2.35 0 2.35c0 0.788 2.507 2.228 5.6 2.228 3.092 0 5.6-1.44 5.6-2.229 0 0 0-0.209 0-2.35s-3.877-3.908-3.877-5.422c0-1.515 3.877-3.282 3.877-5.423zM5.941 2.328c0.696-0.439 2-1.082 4.114-1.082s4.006 1.082 4.006 1.082c0.142 0.086 0.698 0.383 0.317 0.609-0.838 0.497-2.478 1.020-4.378 1.020s-3.484-0.576-4.324-1.074c-0.381-0.225 0.265-0.555 0.265-0.555zM10.501 10c0 1.193 0.996 1.961 2.051 2.986 0.771 0.748 1.826 1.773 1.826 2.435v1.328c-0.97-0.483-3.872-0.955-3.872-2.504 0-0.783-1.013-0.783-1.013 0 0 1.549-2.902 2.021-3.872 2.504v-1.328c0-0.662 1.056-1.688 1.826-2.435 1.055-1.025 2.051-1.793 2.051-2.986s-0.996-1.961-2.051-2.986c-0.771-0.75-1.826-1.775-1.826-2.438l-0.046-0.998c1.026 0.553 2.652 1.078 4.425 1.078 1.772 0 3.406-0.525 4.433-1.078l-0.055 0.998c0 0.662-1.056 1.688-1.826 2.438-1.054 1.025-2.051 1.793-2.051 2.986z'%3E%3C/path%3E%3C/svg%3E%0A\");\n  @include normal-actions;\n  vertical-align: baseline;\n  width: 1em;\n  height: 1em;\n\n\n  .theme-dark & {\n    background-image: url(\"data:image/svg+xml,%3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Ctitle%3Ehour-glass%3C/title%3E%3Cpath d='M15.6 4.576c0-2.139 0-2.348 0-2.348 0-0.789-2.508-2.228-5.6-2.228-3.093 0-5.6 1.439-5.6 2.228 0 0 0 0.209 0 2.348 0 2.141 3.877 3.908 3.877 5.424 0 1.514-3.877 3.281-3.877 5.422s0 2.35 0 2.35c0 0.788 2.507 2.228 5.6 2.228 3.092 0 5.6-1.44 5.6-2.229 0 0 0-0.209 0-2.35s-3.877-3.908-3.877-5.422c0-1.515 3.877-3.282 3.877-5.423zM5.941 2.328c0.696-0.439 2-1.082 4.114-1.082s4.006 1.082 4.006 1.082c0.142 0.086 0.698 0.383 0.317 0.609-0.838 0.497-2.478 1.020-4.378 1.020s-3.484-0.576-4.324-1.074c-0.381-0.225 0.265-0.555 0.265-0.555zM10.501 10c0 1.193 0.996 1.961 2.051 2.986 0.771 0.748 1.826 1.773 1.826 2.435v1.328c-0.97-0.483-3.872-0.955-3.872-2.504 0-0.783-1.013-0.783-1.013 0 0 1.549-2.902 2.021-3.872 2.504v-1.328c0-0.662 1.056-1.688 1.826-2.435 1.055-1.025 2.051-1.793 2.051-2.986s-0.996-1.961-2.051-2.986c-0.771-0.75-1.826-1.775-1.826-2.438l-0.046-0.998c1.026 0.553 2.652 1.078 4.425 1.078 1.772 0 3.406-0.525 4.433-1.078l-0.055 0.998c0 0.662-1.056 1.688-1.826 2.438-1.054 1.025-2.051 1.793-2.051 2.986z' fill='red'%3E%3C/path%3E%3C/svg%3E%0A\");\n  }\n}\n\n\n// Statblock PF2E Tweaks\n\n@mixin statblock-callout-actions {\n  content: \"\";\n  color: transparent;\n}\n\n.callout[data-callout=\"statblock-pf2e\"] {\n  & a.internal-link[href$=\"#Actions\"][title=\"Single Action\"] {\n    background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 2040 1024'%3E%3Cpath d='M1022 512L510 0 263 247l265 265-265 265 247 247 512-512zM185 329L2 512l183 183 183-183-183-183z'/%3E%3C/svg%3E\");\n    background-size: cover;\n    background-repeat: no-repeat;\n    display: inline-block;\n    vertical-align: top;\n    width: 1em;\n    height: 1em;\n\n    @include statblock-callout-actions;\n  }\n\n  & a.internal-link[href$=\"#Actions\"][title=\"Two-Action\"] {\n    background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 2040 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1018 512L507 1 261 248l264 264-264 264 246 247 511-511zM183 329L0 512l183 183 183-183-183-183zm1417 175L1152 56 935 272l232 232-232 232 217 217 448-449z'/%3E%3C/svg%3E\");\n    background-repeat: no-repeat;\n    background-size: 1.5625rem;\n    line-height: 1.5625rem;\n    vertical-align: top;\n    width: 1em;\n    height: 1em;\n\n    @include statblock-callout-actions;\n  }\n\n  & a.internal-link[href$=\"#Actions\"][title=\"Three-Action\"] {\n    background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 2128 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m1025 512-512-512-247 247 265 265-265 265 247 247zm1098-10-360-360-175 174 187 186-187 187 175 174zm-1935-173-183 183 183 183 183-183zm1427 175-449-449-218 217 233 232-233 233 218 217z'/%3E%3C/svg%3E\");\n    background-repeat: no-repeat;\n    line-height: 1.5625rem;\n    vertical-align: top;\n    width: 1em;\n    height: 1em;\n\n    @include statblock-callout-actions;\n  }\n\n  & a.internal-link[href$=\"#Actions\"][title=\"Reaction\"] {\n    background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 2128 1024'%3E%3Cpath d='M1013 377q-23-71-82-128-60-57-143.5-94T603 105q-101-14-207 1-75 11-140 35t-116 58q-51 34-87 77-36 42-53 89 21-26 50-48 29-23 64.5-41t76.5-31q42-13 88-20 85-12 166-1t148.5 40.5Q661 294 709 340t66 103q18 58 2 113-17 55-60.5 100.5T605 735q-67 32-152 44-28 4-55.5 5.5t-54.5.5q63 14 131.5 17t137.5-7q106-15 190-56 84-40 138-96.5t75-125.5q20-68-2-140zM152 823l489 103-109-149 56-215-436 261z'/%3E%3C/svg%3E\");\n    background-size: cover;\n    display: inline-block;\n    vertical-align: middle;\n    width: 1em;\n    height: 1em;\n\n    @include statblock-callout-actions;\n  }\n\n  & a.internal-link[href$=\"#Actions\"][title=\"Free Action\"] {\n    background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 2128 1024'%3E%3Cpath d='M516 0L267 250 0 518l509 506 515-515L516 0zM188 496l109-109 109 109-109 109-109-109zm311 380L386 763l251-251-245-246 124-124 359 359-376 375z'/%3E%3C/svg%3E\");\n    background-size: cover;\n    display: inline-block;\n    vertical-align: top;\n    width: 1em;\n    height: 1em;\n\n    @include statblock-callout-actions;\n  }\n\n  & a.internal-link[href$=\"#Actions\"][title=\"Varies\"] {\n    background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpath d='M16 2c-7.732 0-14 6.268-14 14s6.268 14 14 14 14-6.268 14-14-6.268-14-14-14zM28.998 15.952c-3.808 1.889-8.19 1.875-11.971 0 3.625-2.404 5.958-6.273 6.398-10.608 3.352 2.343 5.557 6.22 5.573 10.608zM22.46 4.735c-0.273 4.234-2.484 8.022-5.989 10.353-0.268-4.347-2.447-8.296-5.983-10.845 1.677-0.789 3.54-1.243 5.512-1.243 2.352 0 4.555 0.638 6.46 1.735zM9.471 4.776c3.532 2.354 5.708 6.151 5.974 10.359-3.9-1.941-8.413-2.028-12.389-0.238 0.365-4.322 2.853-8.040 6.415-10.121zM3.002 16.047c3.808-1.887 8.192-1.873 11.972 0.001-3.628 2.405-5.96 6.27-6.4 10.607-3.352-2.343-5.556-6.22-5.572-10.608zM9.542 27.266c0.273-4.236 2.478-8.021 5.987-10.354 0.268 4.352 2.447 8.295 5.984 10.844-1.676 0.79-3.54 1.244-5.513 1.244-2.352 0-4.553-0.637-6.458-1.734zM22.531 27.222c-3.534-2.354-5.711-6.145-5.976-10.359 2.035 1.013 4.237 1.523 6.442 1.523 2.025 0 4.045-0.455 5.949-1.314-0.357 4.334-2.845 8.064-6.415 10.15z'%3E%3C/path%3E%3C/svg%3E\");\n    background-size: cover;\n    display: inline-block;\n    vertical-align: top;\n    width: 1em;\n    height: 1em;\n\n    @include statblock-callout-actions;\n  }\n\n  & a.internal-link[href$=\"#Actions\"][title=\"Duration or Frequency\"] {\n    background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' version='1.1' width='20' height='20' viewBox='0 0 20 20'%3E%3Ctitle%3Ehour-glass%3C/title%3E%3Cpath d='M15.6 4.576c0-2.139 0-2.348 0-2.348 0-0.789-2.508-2.228-5.6-2.228-3.093 0-5.6 1.439-5.6 2.228 0 0 0 0.209 0 2.348 0 2.141 3.877 3.908 3.877 5.424 0 1.514-3.877 3.281-3.877 5.422s0 2.35 0 2.35c0 0.788 2.507 2.228 5.6 2.228 3.092 0 5.6-1.44 5.6-2.229 0 0 0-0.209 0-2.35s-3.877-3.908-3.877-5.422c0-1.515 3.877-3.282 3.877-5.423zM5.941 2.328c0.696-0.439 2-1.082 4.114-1.082s4.006 1.082 4.006 1.082c0.142 0.086 0.698 0.383 0.317 0.609-0.838 0.497-2.478 1.020-4.378 1.020s-3.484-0.576-4.324-1.074c-0.381-0.225 0.265-0.555 0.265-0.555zM10.501 10c0 1.193 0.996 1.961 2.051 2.986 0.771 0.748 1.826 1.773 1.826 2.435v1.328c-0.97-0.483-3.872-0.955-3.872-2.504 0-0.783-1.013-0.783-1.013 0 0 1.549-2.902 2.021-3.872 2.504v-1.328c0-0.662 1.056-1.688 1.826-2.435 1.055-1.025 2.051-1.793 2.051-2.986s-0.996-1.961-2.051-2.986c-0.771-0.75-1.826-1.775-1.826-2.438l-0.046-0.998c1.026 0.553 2.652 1.078 4.425 1.078 1.772 0 3.406-0.525 4.433-1.078l-0.055 0.998c0 0.662-1.056 1.688-1.826 2.438-1.054 1.025-2.051 1.793-2.051 2.986z'%3E%3C/path%3E%3C/svg%3E%0A\");\n    background-size: cover;\n    display: inline-block;\n    vertical-align: top;\n    width: 1em;\n    height: 1em;\n\n    @include statblock-callout-actions;\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2e/styling/_embeds.scss",
    "content": "// ------------------------------------------------------\n// Naked embeds.. This code should look familiar ;)\n// Credits to Ebullient for most of this One\n// ------------------------------------------------------\n// Variables\n@use '../pf2e-variables' as var;\n$svg-area: 18px;\n\n// Mixin for common styles\n@mixin embed-common-styles() {\n  border: none;\n  border-radius: 0;\n  margin: 0;\n  padding: 0;\n  position: relative;\n}\n\n:is(.pf2e) {\n   .markdown-embed {\n    // Common styles\n    @include embed-common-styles();\n\n    // Hide embedded header and create uniform padding\n    & .embed-title,\n    .mod-header {\n      display: none;\n      gap: 0;\n      padding: 0;\n    }\n\n    // Style link and file embeds\n    &-link svg,\n    .file-embed-link {\n      height: $svg-area;\n      width: $svg-area;\n      right: 0;\n      top: 0;\n      text-align: center;\n      vertical-align: middle;\n    }\n\n    // Style source and rendered views\n    &-source-view.invisible-embed,\n    &-rendered.invisible-embed {\n      --embed-border-left: 0;\n      --embed-border-right: 0;\n      --embed-padding: 0;\n    }\n\n    .markdown-embed-content {\n      // Remove max height and overflow\n      max-height: unset;\n      overflow: unset;\n\n      > .markdown-preview-view {\n        // Remove vertical scroll\n        overflow-y: unset;\n      }\n    }\n  }\n\n  .markdown-source-view {\n    &.internal-embed.markdown-embed {\n      &-title {\n        // Common styles\n        @include embed-common-styles();\n\n        display: none;\n        gap: 0;\n        font-size: 0;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2e/styling/_index.scss",
    "content": "@forward 'action-icons';\n@forward 'trait-box-shadows';\n@forward 'embeds';\n@forward 'link-text';\n@forward 'pathfinder-font-classes';\n@forward 'scrollbars';\n@forward 'sub-sup';\n@forward 'table';\n"
  },
  {
    "path": "src/scss/pf2e/styling/_link-text.scss",
    "content": "// Variables\n@use '../pf2e-variables' as var;\n\n\n@mixin hover-link {\n  text-decoration: underline;\n}\n\n@mixin no-underline {\n  text-decoration: none;\n}\n\n:is(.pf2e) {\n  a.internal-link {\n    @include no-underline;\n    color: var(--pf2e-blue-link);\n    filter: brightness(1.0);\n\n    .theme-dark & {\n      filter: brightness(1.2);\n    }\n\n\n    &:hover {\n      @include hover-link;\n      color: var(--pf2e-green-link);\n    }\n  }\n\n  .cm-link .cm-underline,\n  .cm-url .cm-underline {\n    color: var(--pf2e-blue-link);\n    @include no-underline;\n\n    &:hover {\n      color: var(--pf2e-green-link);\n      @include hover-link;\n    }\n  }\n\n  .external-link {\n    color: var(--pf2e-green-link);\n    background-image: none;\n    background-size: 0;\n    font-style: italic;\n    padding-right: 0;\n\n    &:hover {\n      color: var(--pf2e-purple-link);\n      @include hover-link;\n    }\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2e/styling/_pathfinder-font-classes.scss",
    "content": "// Variables\n@use '../pf2e-variables' as var;\n\n[class^='Pathfinder-'], [class*=' Pathfinder-'] {\n  /* use !important to prevent issues with browser extensions that change fonts */\n  font-family: 'Pathfinder', serif !important;\n  -webkit-font-smoothing: antialiased;\n  font-style: normal;\n  font-variant: normal;\n  font-weight: normal;\n  line-height: 1;\n\n  /* Better Font Rendering =========== */\n  -moz-osx-font-smoothing: grayscale;\n  text-transform: none;\n}\n\n.pathfinder-1action {\n  &::before {\n    content: \"\\2b3b\";\n  }\n}\n\n.pathfinder-2actions {\n  &::before {\n    content: \"\\2b3a\";\n  }\n}\n\n.pathfinder-3actions {\n  &::before {\n    content: \"\\2b3d\";\n  }\n}\n\n.pathfinder-delay {\n  &::before {\n    content: \"\\2b53\";\n  }\n}\n\n.pathfinder-reaction {\n  &::before {\n    content: \"\\2b32\";\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2e/styling/_scrollbars.scss",
    "content": "// Scrollbars for Callouts\n:is(.pf2e) {\n  &:is(.callout, .admonition) {\n    &:hover {\n      scrollbar-color: var(--scrollbar-active-thumb-bg) transparent;\n    }\n\n    &::-webkit-scrollbar {\n      width: 12px;\n    }\n\n    &::-webkit-scrollbar-track {\n      background: var(--scrollbar-hover);\n      border: 0.25rem solid transparent;\n      background-clip: content-box;\n    }\n\n    &::-webkit-scrollbar-thumb {\n      background: var(--scrollbar-active-thumb-bg);\n      border: 0.0625rem solid var(--scrollbar-thumb-bg);\n    }\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2e/styling/_sub-sup.scss",
    "content": "// Variables\n@use '../pf2e-variables' as var;\n\n// Common-Mixin\n@mixin shared-sub-sup {\n  font-size: 75%;\n  line-height: 0;\n  position: relative;\n  text-align: center;\n  vertical-align: baseline;\n}\n\n// Individual Mixins\nsub {\n  @include shared-sub-sup;\n  bottom: -0.25em;\n}\n\nsup {  @include shared-sub-sup;\n  top: -0.5em;\n}\n"
  },
  {
    "path": "src/scss/pf2e/styling/_table.scss",
    "content": "// Variables\n@use '../pf2e-variables' as var;\n\n:is(.pf2e) {\n  .markdown-rendered {\n    table {\n      border: 0.1875rem solid var(--pf2e-red-headers);\n\n      a {\n        font-weight: 500;\n        color: var(--pf2e-purple-link);\n      }\n    }\n\n    thead {\n      background-color: var(--pf2e-red-headers);\n      border-bottom: 0.1875rem solid rgba(var(--pf2e-brown-table-borders-rgb), 0.5);\n      color: var.$very-light-grey;\n    }\n\n    tbody {\n      background-color: rgb(var(--pf2e-tan-table-odd-rows-rgb));\n      color: var.$almost-black;\n\n      tr:nth-child(even) {\n        background-color: rgb(var(--pf2e-beige-table-even-rows-rgb));\n      }\n\n      tr:hover {\n        background-color: rgba(var(--pf2e-table-hover-color), 0.8);\n      }\n\n    }\n\n    th {\n      background-color: var(--pf2e-red-headers);\n      color: var.$very-light-grey;\n\n      &:hover {\n        background-color: rgba(var(--pf2e-table-hover-color), 0.8);\n      }\n    }\n\n    tr td:not(:last-child),\n    tr th:not(:last-child) {\n      border-right: 0.0625rem solid var.$light-grayish-yellow;\n    }\n  }\n}\n"
  },
  {
    "path": "src/scss/pf2e/styling/_trait-box-shadows.scss",
    "content": "// Variables\n@use '../pf2e-variables' as var;\n\n// Mixins\n\n@mixin link-styles {\n  background-clip: border-box;\n  border-radius: 0.3125rem;\n  border-style: solid;\n  color: rgb(232, 232, 232) !important;\n  cursor: help;\n  font-weight: 700;\n  padding: 0.0625rem;\n  margin: 0.125rem;\n  text-align: center;\n  text-indent: 0;\n  word-break: keep-all;\n  font-style: normal;\n  display: inline-block;\n\n  border-width: 0.0625rem;\n\n  li & {\n    margin-bottom: 0;\n  }\n\n  .callout[data-callout=\"statblock-pf2e\"] & {\n    margin-bottom: 0.75rem;\n  }\n}\n\na.internal-link {\n\n  // Rarity Traits\n\n  &[title=\"Common Rarity Trait\"] {\n    background: rgb(232, 232, 232);\n    border-color: rgb(232, 232, 232);\n\n    background-clip: border-box;\n    border-radius: 0.3125rem;\n    border-style: solid;\n    color: rgb(0, 0, 0) !important;\n    cursor: help;\n    font-weight: 700;\n    padding: 0.0625rem;\n    margin: 0.125rem;\n    text-align: center;\n    text-indent: 0;\n    word-break: keep-all;\n    font-style: normal;\n    display: inline-block;\n\n    border-width: 0.0625rem;\n\n    li & {\n      margin-bottom: 0;\n    }\n\n    .callout[data-callout=\"statblock-pf2e\"] & {\n      margin-bottom: 0.75rem;\n    }\n  }\n\n  &[title=\"Uncommon Rarity Trait\"] {\n    background: rgb(152, 81, 61);\n    border-color: rgb(152, 81, 61);\n\n    @include link-styles;\n  }\n\n  &[title=\"Rare Rarity Trait\"] {\n    background: rgb(0, 38, 100);\n    border-color: rgb(0, 38, 100);\n\n    @include link-styles;\n  }\n\n  &[title=\"Unique Rarity Trait\"] {\n    background: rgb(84, 22, 110);\n    border-color: rgb(84, 22, 110);\n\n    @include link-styles;\n  }\n\n  // Alignment\n\n  &[title*=\"Alignment Trait\"] {\n    background: rgb(102, 111, 153);\n    border-color: rgb(102, 111, 153);\n\n    @include link-styles;\n  }\n\n  // Sizing\n\n  &[title*=\"Size Trait\"] {\n    background: rgb(82, 122, 95);\n    border-color: rgb(82, 122, 95);\n\n    @include link-styles;\n  }\n\n  // Other Traits\n  &[title*=\"Action & Ability Trait\"],\n  &[title*=\"Ancestry & Heritage Trait\"],\n  &[title*=\"Armor Trait\"],\n  &[title*=\"Class Trait\"],\n  &[title*=\"Combat Trait\"],\n  &[title*=\"Creature Trait\"],\n  &[title*=\"Creature Type Trait\"],\n  &[title*=\"Effect Trait\"],\n  &[title*=\"Element Trait\"],\n  &[title*=\"Equipment Trait\"],\n  &[title*=\"Feat Trait\"],\n  &[title*=\"General Trait\"],\n  &[title*=\"Gravity Trait\"],\n  &[title*=\"Hazard Trait\"],\n  &[title*=\"Item Trait\"],\n  &[title*=\"Morphic Trait\"],\n  &[title*=\"Planar Trait\"],\n  &[title*=\"Settlement Trait\"],\n  &[title*=\"School Trait\"],\n  &[title*=\"Spell Trait\"],\n  &[title*=\"Tradition Trait\"],\n  &[title*=\"Weapon Trait\"] {\n    background: rgb(97, 20, 5);\n    border-color: rgb(97, 20, 5);\n\n    @include link-styles;\n  }\n\n  //// Conditions\n  //\n  //&[href*=\"rules/conditions\"] {\n  //  border: 0.0625rem solid rgb(19, 138, 6);\n  //  background: rgb(19, 138, 6);\n  //  @include statblock-link-styles;\n  //}\n}\n"
  },
  {
    "path": "src/test/java/dev/ebullient/convert/CustomTemplatesIT.java",
    "content": "package dev.ebullient.convert;\n\nimport org.junit.jupiter.api.BeforeAll;\n\nimport io.quarkus.test.junit.main.QuarkusMainIntegrationTest;\n\n@QuarkusMainIntegrationTest\npublic class CustomTemplatesIT extends CustomTemplatesTest {\n    @BeforeAll\n    public static void setupDir() {\n        setupDir(\"templates-IT\");\n    }\n}\n"
  },
  {
    "path": "src/test/java/dev/ebullient/convert/CustomTemplatesTest.java",
    "content": "package dev.ebullient.convert;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.io.IOException;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardCopyOption;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.tools.dnd5e.Tools5eIndexType;\nimport dev.ebullient.convert.tools.dnd5e.Tools5eLinkifier;\nimport io.quarkus.test.junit.main.Launch;\nimport io.quarkus.test.junit.main.LaunchResult;\nimport io.quarkus.test.junit.main.QuarkusMainLauncher;\nimport io.quarkus.test.junit.main.QuarkusMainTest;\nimport picocli.CommandLine;\n\n@QuarkusMainTest\npublic class CustomTemplatesTest {\n    static Path rootTestOutput;\n    static Tui tui;\n\n    Path testOutput;\n\n    @BeforeAll\n    public static void setupDir() {\n        setupDir(\"templates\");\n    }\n\n    public static void setupDir(String name) {\n        tui = new Tui();\n        tui.init(null, false, false);\n        rootTestOutput = TestUtils.OUTPUT_ROOT_5E.resolve(name);\n        rootTestOutput.toFile().mkdirs();\n    }\n\n    @AfterAll\n    public static void cleanup() {\n        System.out.println(\"Done.\");\n    }\n\n    @BeforeEach\n    public void setup() {\n        testOutput = null; // test should set this to something readable\n    }\n\n    @AfterEach\n    public void moveLogFile() throws IOException {\n        assertThat(testOutput).isNotNull(); // make sure test set this\n\n        Path logFile = Path.of(\"ttrpg-convert.out.txt\");\n        if (Files.exists(logFile) && Files.exists(testOutput)) {\n            String content = Files.readString(logFile, StandardCharsets.UTF_8);\n\n            Path filePath = testOutput.resolve(logFile);\n            Files.move(logFile, filePath, StandardCopyOption.REPLACE_EXISTING);\n\n            if (content.contains(\"Exception\")) {\n                tui.errorf(\"Exception found in %s\", filePath);\n            }\n        }\n        TestUtils.cleanupReferences();\n    }\n\n    @Test\n    @Launch({ \"--help\" })\n    void testCommandHelp(LaunchResult result) {\n        testOutput = rootTestOutput.resolve(\"help\");\n        result.echoSystemOut();\n        assertThat(result.getOutput())\n                .withFailMessage(\"Usage statement not found in output. Output:%n%s\", TestUtils.dump(result))\n                .contains(\"Usage:\");\n    }\n\n    @Test\n    @Launch({ \"--version\" })\n    void testCommandVersion(LaunchResult result) {\n        testOutput = rootTestOutput.resolve(\"version\");\n        result.echoSystemOut();\n        assertThat(result.getOutput())\n                .withFailMessage(\"Version statement not found in output. Output:%n%s\", TestUtils.dump(result))\n                .contains(\"ttrpg-convert version\");\n    }\n\n    @Test\n    void testCommandBadTemplates(QuarkusMainLauncher launcher) throws IOException {\n        testOutput = rootTestOutput.resolve(\"bad-templates\");\n        if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) {\n            TestUtils.deleteDir(testOutput);\n            LaunchResult result = launcher.launch(\"--index\",\n                    \"--background=garbage.txt\",\n                    \"-o\", testOutput.toString(),\n                    TestUtils.TEST_RESOURCES.resolve(\"5e/images-remote.json\").toString(),\n                    TestUtils.PATH_5E_TOOLS_DATA.toString());\n\n            assertThat(result.exitCode())\n                    .withFailMessage(\"Command did not fail as expected. Output:%n%s\", TestUtils.dump(result))\n                    .isEqualTo(CommandLine.ExitCode.USAGE);\n        }\n    }\n\n    @Test\n    void testCommandBadTemplatesInJson(QuarkusMainLauncher launcher) throws IOException {\n        testOutput = rootTestOutput.resolve(\"bad-templates-json\");\n        if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) {\n            TestUtils.deleteDir(testOutput);\n\n            LaunchResult result = launcher.launch(\"--index\",\n                    \"-o\", testOutput.toString(),\n                    TestUtils.PATH_5E_TOOLS_DATA.toString(),\n                    TestUtils.TEST_RESOURCES.resolve(\"5e/images-remote.json\").toString(),\n                    TestUtils.TEST_RESOURCES.resolve(\"sources-bad-template.json\").toString());\n\n            assertThat(result.exitCode())\n                    .withFailMessage(\"Command did not fail as expected. Output:%n%s\", TestUtils.dump(result))\n                    .isEqualTo(CommandLine.ExitCode.USAGE);\n        }\n    }\n\n    @Test\n    void testCommandTemplates_5e(QuarkusMainLauncher launcher) throws IOException {\n        testOutput = rootTestOutput.resolve(\"srd-templates\");\n        if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) {\n            TestUtils.deleteDir(testOutput);\n\n            // SRD only, just templates\n            LaunchResult result = launcher.launch(\"--log\", \"--index\",\n                    \"--background\", TestUtils.TEST_RESOURCES.resolve(\"other/background.txt\").toString(),\n                    \"--class\", TestUtils.TEST_RESOURCES.resolve(\"other/class.txt\").toString(),\n                    \"--deity\", TestUtils.TEST_RESOURCES.resolve(\"other/deity.txt\").toString(),\n                    \"--feat\", TestUtils.TEST_RESOURCES.resolve(\"other/feat.txt\").toString(),\n                    \"--item\", TestUtils.TEST_RESOURCES.resolve(\"other/item.txt\").toString(),\n                    \"--note\", TestUtils.TEST_RESOURCES.resolve(\"other/note.txt\").toString(),\n                    \"--race\", TestUtils.TEST_RESOURCES.resolve(\"other/race.txt\").toString(),\n                    \"--spell\", TestUtils.TEST_RESOURCES.resolve(\"other/spell.txt\").toString(),\n                    \"--subclass\", TestUtils.TEST_RESOURCES.resolve(\"other/subclass.txt\").toString(),\n                    \"-o\", testOutput.toString(),\n                    TestUtils.TEST_RESOURCES.resolve(\"5e/images-remote.json\").toString(),\n                    TestUtils.PATH_5E_TOOLS_DATA.toString());\n\n            assertThat(result.exitCode())\n                    .withFailMessage(\"Command failed. Output:%n%s\", TestUtils.dump(result))\n                    .isEqualTo(0);\n\n            List.of(\n                    testOutput.resolve(\"compendium/backgrounds\"),\n                    testOutput.resolve(\"compendium/classes\"),\n                    testOutput.resolve(\"compendium/feats\"),\n                    testOutput.resolve(\"compendium/items\"),\n                    testOutput.resolve(\"compendium/races\"),\n                    testOutput.resolve(\"compendium/spells\"),\n                    testOutput.resolve(\"rules\"))\n                    .forEach(directory -> TestUtils.assertDirectoryContents(directory, tui, (p, content) -> {\n                        List<String> errors = new ArrayList<>();\n                        boolean index = false;\n                        boolean frontmatter = false;\n                        boolean foundTestTag = false;\n                        for (String l : content) {\n                            if (\"---\".equals(l)) {\n                                frontmatter = !frontmatter;\n                            } else if (frontmatter && l.equals(\"- test\")) {\n                                foundTestTag = true;\n                            } else if (l.startsWith(\"# Index \")) {\n                                index = true;\n                            } else if (l.startsWith(\"# \") && !l.matches(\"^# \\\\[.*]\\\\(.*\\\\)\")) {\n                                errors.add(\n                                        String.format(\"H1 does not contain markdown link in %s: %s\", p.toString(), l));\n                            }\n                            if (l.startsWith(\"# \")) {\n                                break;\n                            }\n                        }\n\n                        if (!index && !foundTestTag) {\n                            errors.add(\"Unable to find the - test tag in file \" + p);\n                        }\n                        return errors;\n                    }));\n        }\n    }\n\n    @Test\n    void testCommandTemplates_5eJson(QuarkusMainLauncher launcher) throws IOException {\n        testOutput = rootTestOutput.resolve(\"json-templates\");\n        if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) {\n            TestUtils.deleteDir(testOutput);\n\n            LaunchResult result = launcher.launch(\"--log\", \"--index\",\n                    \"-c\", TestUtils.TEST_RESOURCES.resolve(\"5e/sources-templates.json\").toString(),\n                    \"-o\", testOutput.toString(),\n                    TestUtils.TEST_RESOURCES.resolve(\"5e/images-remote.json\").toString(),\n                    TestUtils.PATH_5E_TOOLS_DATA.toString());\n\n            assertThat(result.exitCode())\n                    .withFailMessage(\"Command failed. Output:%n%s\", TestUtils.dump(result))\n                    .isEqualTo(0);\n\n            // test extra cp value attribute in yaml frontmatter\n            Path abacus = testOutput.resolve(\"compendium/items/%s.md\".formatted(\n                    Tools5eLinkifier.instance().getTargetFileName(\"Abacus\", \"PHB\", Tools5eIndexType.item)));\n            assertThat(abacus).exists();\n            assertThat(abacus).content().contains(\"cost: 200\");\n\n            List.of(\n                    testOutput.resolve(\"compendium/backgrounds\"),\n                    testOutput.resolve(\"compendium/bestiary\"),\n                    testOutput.resolve(\"compendium/classes\"),\n                    testOutput.resolve(\"compendium/deities\"),\n                    testOutput.resolve(\"compendium/feats\"),\n                    testOutput.resolve(\"compendium/items\"),\n                    testOutput.resolve(\"compendium/races\"),\n                    testOutput.resolve(\"compendium/spells\"),\n                    testOutput.resolve(\"rules\"))\n                    .forEach(directory -> TestUtils.assertDirectoryContents(directory, tui, (p, content) -> {\n                        List<String> errors = new ArrayList<>();\n                        boolean frontmatter = false;\n                        boolean foundTestTag = false;\n                        for (String l : content) {\n                            if (\"---\".equals(l)) {\n                                frontmatter = !frontmatter;\n                            } else if (frontmatter && l.equals(\"- test\")) {\n                                foundTestTag = true;\n                            } else if (l.startsWith(\"# \")) {\n                                if (l.contains(\"\\\\\")) {\n                                    errors.add(\n                                            String.format(\"Backslash in heading/link in %s: %s\", p.toString(), l));\n                                }\n                                break;\n                            }\n                        }\n\n                        if (!foundTestTag) {\n                            errors.add(\"Unable to find the - test tag in file \" + p);\n                        }\n                        return errors;\n                    }));\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/java/dev/ebullient/convert/Pf2eDataConvertIT.java",
    "content": "package dev.ebullient.convert;\n\nimport org.junit.jupiter.api.BeforeAll;\n\nimport io.quarkus.test.junit.main.QuarkusMainIntegrationTest;\n\n@QuarkusMainIntegrationTest\npublic class Pf2eDataConvertIT extends Pf2eDataConvertTest {\n    @BeforeAll\n    public static void setupDir() {\n        setupDir(\"test-cli-IT\");\n    }\n}\n"
  },
  {
    "path": "src/test/java/dev/ebullient/convert/Pf2eDataConvertTest.java",
    "content": "package dev.ebullient.convert;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.io.IOException;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardCopyOption;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport dev.ebullient.convert.io.Tui;\nimport io.quarkus.test.junit.main.LaunchResult;\nimport io.quarkus.test.junit.main.QuarkusMainLauncher;\nimport io.quarkus.test.junit.main.QuarkusMainTest;\n\n@QuarkusMainTest\npublic class Pf2eDataConvertTest {\n    static Path testOutputRoot;\n    static Tui tui;\n\n    Path testOutput;\n\n    @BeforeAll\n    public static void setupDir() {\n        setupDir(\"test-cli\");\n    }\n\n    public static void setupDir(String name) {\n        tui = new Tui();\n        tui.init(null, false, false);\n        testOutputRoot = TestUtils.OUTPUT_ROOT_PF2.resolve(name);\n        testOutputRoot.toFile().mkdirs();\n    }\n\n    @AfterAll\n    public static void cleanup() {\n        System.out.println(\"Done.\");\n    }\n\n    @BeforeEach\n    public void setup() {\n        testOutput = null; // test should set this to something readable\n    }\n\n    @AfterEach\n    public void clear() throws IOException {\n        assertThat(testOutput).isNotNull(); // make sure test set this\n\n        Path logFile = Path.of(\"ttrpg-convert.out.txt\");\n        if (Files.exists(logFile)) {\n            Path filePath = testOutput.resolve(logFile);\n            Files.move(logFile, filePath, StandardCopyOption.REPLACE_EXISTING);\n\n            String content = Files.readString(filePath, StandardCharsets.UTF_8);\n            if (content.contains(\"Exception\")) {\n                tui.errorf(\"Exception found in %s\", filePath);\n            }\n        }\n        TestUtils.cleanupReferences();\n    }\n\n    @Test\n    void testLiveData_Pf2eAllSources(QuarkusMainLauncher launcher) {\n        testOutput = testOutputRoot.resolve(\"all-index\");\n        if (TestUtils.PATH_PF2E_TOOLS_DATA.toFile().exists()) {\n            // All, I mean it. Really for real.. ALL.\n            TestUtils.deleteDir(testOutput);\n\n            List<String> args = new ArrayList<>(List.of(\"--index\", \"--log\",\n                    \"-o\", testOutput.toString(),\n                    \"-g\", \"pf2e\",\n                    TestUtils.TEST_RESOURCES.resolve(\"sources-from-all.json\").toString(),\n                    TestUtils.PATH_PF2E_TOOLS_DATA.toString()));\n\n            args.addAll(TestUtils.getFilesFrom(TestUtils.PATH_PF2E_TOOLS_DATA.resolve(\"adventure\"))\n                    .stream()\n                    .filter(x -> !x.endsWith(\"-id.json\"))\n                    .toList());\n            args.addAll(TestUtils.getFilesFrom(TestUtils.PATH_PF2E_TOOLS_DATA.resolve(\"book\")));\n\n            LaunchResult result = launcher.launch(args.toArray(new String[0]));\n            assertThat(result.exitCode())\n                    .withFailMessage(\"Command failed. Output:%n%s\", TestUtils.dump(result))\n                    .isEqualTo(0);\n\n            Tui tui = new Tui();\n            tui.init(null, false, false);\n\n            TestUtils.assertDirectoryContents(testOutput, tui, (p, content) -> {\n                List<String> errors = new ArrayList<>();\n                content.forEach(l -> TestUtils.checkMarkdownLink(testOutput.toString(), p, l, errors));\n                return errors;\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/java/dev/ebullient/convert/TestUtils.java",
    "content": "package dev.ebullient.convert;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport org.junit.jupiter.api.Assertions;\n\nimport dev.ebullient.convert.config.TtrpgConfig;\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.tools.JsonTextConverter;\nimport dev.ebullient.convert.tools.dnd5e.Tools5eIndex;\nimport io.quarkus.test.junit.main.LaunchResult;\n\npublic class TestUtils {\n    public final static boolean USING_MAVEN = System.getProperty(\"maven.home\") != null;\n\n    public final static Path PROJECT_PATH = Paths.get(System.getProperty(\"user.dir\")).toAbsolutePath();\n\n    public final static Path OUTPUT_ROOT_5E = PROJECT_PATH.resolve(\"target/test-5e\");\n    public final static Path OUTPUT_5E_DATA = OUTPUT_ROOT_5E.resolve(USING_MAVEN ? \"data\" : \"data-ide\");\n\n    public final static Path OUTPUT_ROOT_PF2 = PROJECT_PATH.resolve(\"target/test-pf2\");\n\n    public final static Path TEST_RESOURCES = PROJECT_PATH.resolve(\"src/test/resources/\");\n\n    // for compile/test purposes. Must clone/sync separately.\n    public final static Path PATH_5E_TOOLS_SRC = PROJECT_PATH.resolve(\"sources/5etools-src\");\n    public final static Path PATH_5E_TOOLS_DATA = PATH_5E_TOOLS_SRC.resolve(\"data\");\n    public final static Path PATH_5E_TOOLS_IMAGES = PROJECT_PATH.resolve(\"sources/5etools-img\");\n\n    public final static Path PATH_5E_HOMEBREW = PROJECT_PATH.resolve(\"sources/5e-homebrew\");\n    public final static Path PATH_5E_UA = PROJECT_PATH.resolve(\"sources/5e-unearthed-arcana\");\n    public final static Path PATH_PF2E_TOOLS_DATA = PROJECT_PATH.resolve(\"sources/Pf2eTools/data\");\n\n    static String GENERATED_DOCS = PROJECT_PATH.resolve(\"docs/templates\").normalize().toAbsolutePath().toString();\n\n    // Obnoxious regular expression because markdown links are complicated:\n    // Matches: [link text](vaultPath \"title\")\n    //\n    //    [Enspelled (Level 6) Hooked Shortspear](#Enspelled%20(Level%206)%20Hooked%20Shortspear)\n    // - link text is optional, and may contain parentheses. Use a negative lookahead for ](\n    // - vaultPath is required.\n    //   - Slugified file names will not contain spaces\n    //   - Anchors can include parenthesis. Spaces will be escaped with %20.\n    //   - Stop matching the vaultPath if you encounter a space (precedes an optional title) or a following markdown link\n    // - title is optional\n    final static String nextLink = \"(?!\\\\]\\\\()\"; // negative lookahead for ](\n    final static String linkText = \"([^\\\\]]*?)\"; // optional link text as group\n    final static String linkTitle = \"( \\\".+?\\\")?\"; // optional link title\n\n    // Match the vault path.. which could have nested parenthesis\n    final static String vaultPath = \"([^\\\\s\\\"\\\\(\\\\)]+(?:\\\\([^\\\\)]*\\\\)[^\\\\s\\\"\\\\(\\\\)]*)*?)\";\n\n    final static Pattern markdownLinkPattern = Pattern\n            .compile(\"\\\\[\" + linkText + \"\\\\]\\\\(\" + vaultPath + linkTitle + \"\\\\)\");\n    final static Pattern blockRefPattern = Pattern.compile(\"[^#\\\\[]+(\\\\^[^ ]+)\");\n\n    final static Map<Path, List<String>> pathHeadings = new HashMap<>();\n    final static Map<Path, List<String>> pathBlockReferences = new HashMap<>();\n\n    public static void cleanupReferences() {\n        TestUtils.pathHeadings.clear();\n        TestUtils.pathBlockReferences.clear();\n        TtrpgConfig.init(Tui.instance());\n    }\n\n    public static void checkMarkdownLink(String baseDir, Path p, String line, List<String> errors) {\n        Path absPath = p.normalize().toAbsolutePath();\n        if (!absPath.toString().endsWith(\".md\")) {\n            // GH anchor links\n            return;\n        }\n        final boolean githubStyle = !absPath.toString().startsWith(GENERATED_DOCS);\n        List<String> e = new ArrayList<>();\n        // replace (Level x) or (1st level) or (Firearms) with :whatever: to simplify matching\n        Matcher links = markdownLinkPattern.matcher(line);\n        links.results().forEach(m -> {\n            String path = m.group(2);\n            String anchor = null;\n            Path resource = p;\n            int hash = path.indexOf('#');\n\n            if (path.startsWith(\"http\") && path.contains(\" \")) {\n                e.add(String.format(\"HTTP path with space in %s: %s \", p, m.group(0)));\n                return;\n            } else if (path.contains(\"file://https://\")) {\n                e.add(String.format(\"file://https:// in %s: %s \", p, m.group(0)));\n                return;\n            } else if (path.startsWith(\"http\")\n                    || path.startsWith(\"file://\")\n                    || path.contains(\"vaultPath\")\n                    || path.contains(\"#anchor\")\n                    || path.startsWith(\"{it.\")\n                    || path.startsWith(\"{resource.\")) {\n                // template examples, or other non-file links\n                return;\n            }\n\n            if (hash == 0) {\n                anchor = path.substring(1);\n                path = \"\";\n            } else if (hash > 0) {\n                anchor = path.substring(hash + 1);\n                path = path.substring(0, hash);\n            }\n\n            if (!path.isBlank()) {\n                path = path.replace(\"%20\", \" \");\n                if (path.startsWith(\"./\")) {\n                    Path parent = p.getParent();\n                    resource = parent.resolve(path);\n                } else {\n                    resource = Path.of(baseDir, path);\n                }\n\n                if (path.contains(\"\\\\\")) {\n                    e.add(String.format(\"Found backslash in path in %s: %s\", p, m.group(0)));\n                }\n\n                if (!resource.toFile().exists()) {\n                    Path firstResource = resource;\n                    resource = p.getParent().resolve(path);\n                    if (!resource.toFile().exists()) {\n                        System.out.println(\"💈 \" + path\n                                + \"\\n\\t    first:\" + firstResource\n                                + \"\\n\\t   second:\" + resource\n                                + \"\\n\\t   from p:\" + p);\n                        e.add(String.format(\"Unresolvable path (%s) in %s: %s\", path, p, m.group(0)));\n                        return;\n                    }\n                }\n            }\n\n            if (anchor != null) {\n                if (!resource.toString().endsWith(\".md\")) {\n                    return;\n                }\n                if (anchor.startsWith(\"^\")) {\n                    List<String> blockRefs = findBlockRefsIn(resource);\n                    if (!blockRefs.contains(anchor)) {\n                        e.add(String.format(\"Unresolvable block reference (%s) in %s:  %s\", anchor, p, m.group(0)));\n                    }\n                } else {\n                    String heading = simplifyAnchor(anchor).replaceAll(\"%20\", \" \");\n                    String ghHeading = heading.replace(\"-\", \" \");\n                    List<String> headings = findHeadingsIn(resource, githubStyle);\n                    // obsidian or github style anchors\n                    if (!headings.contains(heading) && !headings.contains(ghHeading)) {\n                        e.add(String.format(\"Unresolvable anchor (%s) in %s: %s\", heading, p, m.group(0)));\n                    }\n                }\n            }\n        });\n        errors.addAll(e);\n    }\n\n    public static List<String> findHeadingsIn(Path p, boolean githubStyle) {\n        if (!p.toString().endsWith(\".md\")) {\n            return List.of();\n        }\n        return pathHeadings.computeIfAbsent(p, key -> {\n            List<String> headings = new ArrayList<>();\n            try (Stream<String> lines = Files.lines(key)) {\n                lines.forEach(l -> {\n                    if (l.startsWith(\"#\")) {\n                        if (l.contains(\".\")) {\n                            System.out.println(\"🔮 Found dot in heading in \" + p + \": \" + l);\n                        }\n                        l = simplifyAnchor(l);\n                        if (githubStyle) {\n                            l = l.replace(\"`\", \"\")\n                                    .replace(\"-\", \" \");\n                        }\n                        headings.add(l);\n                    }\n                });\n            } catch (UncheckedIOException | IOException e) {\n                System.err.println(String.format(\"🛑 Error finding headings in %s: %s\", p, e.toString()));\n            }\n            return headings;\n        });\n    }\n\n    private static String simplifyAnchor(String s) {\n        return s.replace(\"#\", \"\")\n                .replace(\".\", \"\")\n                .replace(\":\", \"\")\n                .replace(\"(\", \"\")\n                .replace(\")\", \"\")\n                .toLowerCase()\n                .trim();\n    }\n\n    public static List<String> findBlockRefsIn(Path p) {\n        if (!p.toString().endsWith(\".md\")) {\n            return List.of();\n        }\n        return pathBlockReferences.computeIfAbsent(p, key -> {\n            List<String> blockrefs = new ArrayList<>();\n            try (Stream<String> lines = Files.lines(key)) {\n                lines.forEach(l -> {\n                    if (l.startsWith(\"^\")) {\n                        blockrefs.add(l.trim());\n                    } else {\n                        Matcher blockRefs = blockRefPattern.matcher(l);\n                        blockRefs.results().forEach(m -> {\n                            blockrefs.add(m.group(1));\n                        });\n                    }\n                });\n            } catch (UncheckedIOException | IOException e) {\n                System.err.println(String.format(\"🛑 Error finding block references in %s: %s\", p, e.toString()));\n            }\n            return blockrefs;\n        });\n    }\n\n    /**\n     * Common content tests. Will append an error if the text contains an unresolved reference,\n     * either &#123;@ or &#123;#.\n     *\n     * @param p Path of content\n     * @param l Line of content\n     * @param errors List of errors to append\n     */\n    public static void commonTests(Path p, String l, List<String> errors) {\n        if (l.contains(\"{@\")) {\n            errors.add(String.format(\"Found {@ in %s: %s\", p, l));\n        }\n        if (l.contains(\"{#\")) {\n            errors.add(String.format(\"Found {# in %s: %s\", p, l));\n        }\n        if (l.contains(\"%% ERROR\")) {\n            errors.add(String.format(\"Found template error in %s: %s\", p, l));\n        }\n        if (l.startsWith(\"| dice: \")) {\n            // some dice rolls are d100 + MOD or something. Those are fine.\n            // but some have prompts, and we should catch/replace those, e.g.:\n            // dice: d6 + #$prompt_number:title=Enter a Modifier$#\n            if (!l.matches(JsonTextConverter.DICE_TABLE_HEADER) && l.contains(\"#$\")) {\n                errors.add(String.format(\"Found invalid dice roll in %s: %s\", p, l));\n            }\n        }\n        // Alarm is a basic spell. It should always be linked. If it isn't,\n        // a reference has gone awry somewhere along the way\n        if (p.toString().contains(\"list-spells-\") && l.contains(\" Alarm\")) {\n            errors.add(String.format(\"Missing link to Alarm spell in %s: %s\", p, l));\n        }\n    }\n\n    /**\n     * Consumes a path, and content as a list of strings.\n     * For each line in the content, it will apply {@link #commonTests(Path, String, List)}\n     *\n     * @return array of discovered errors\n     */\n    static final BiFunction<Path, List<String>, List<String>> checkContents = (p, content) -> {\n        List<String> e = new ArrayList<>();\n        content.forEach(l -> commonTests(p, l, e));\n        return e;\n    };\n\n    /**\n     * Look at all files in the directory and perform common content checks\n     *\n     * @param directory Directory to inspect\n     * @param tui Text UI for errors\n     * @see #assertDirectoryContents(Path, Tui, BiFunction)\n     * @see #checkContents\n     */\n    public static void assertDirectoryContents(Path directory, Tui tui) {\n        assertDirectoryContents(directory, tui, checkContents);\n    }\n\n    /**\n     * Look at all files in the directory and apply the provided content checker to each file\n     *\n     * @param directory Directory to inspect\n     * @param tui Text UI for errors\n     * @param checker additional tests to apply to each file in the directory (passed to\n     *        {@link #checkDirectoryContents(Path, Tui, BiFunction)})\n     * @see #checkDirectoryContents\n     */\n    public static void assertDirectoryContents(Path directory, Tui tui, BiFunction<Path, List<String>, List<String>> checker) {\n        List<String> errors = checkDirectoryContents(directory, tui, checker);\n        assertThat(errors).isEmpty();\n    }\n\n    public static void assertMarkdownLinks(Path filePath, Tui tui) {\n        List<String> errors = new ArrayList<>();\n        testMarkdownLinks(filePath, tui, errors);\n        assertThat(errors).isEmpty();\n    }\n\n    private static void testMarkdownLinks(Path filePath, Tui tui, List<String> errors) {\n        if (filePath.toFile().isDirectory()) {\n            try (Stream<Path> walk = Files.list(filePath)) {\n                walk.forEach(p -> {\n                    testMarkdownLinks(p, tui, errors);\n                });\n            } catch (IOException e) {\n                e.printStackTrace();\n                errors.add(String.format(\"Unable to parse files in directory %s: %s\", filePath, e));\n            }\n        } else if (filePath.endsWith(\".md\")) {\n            try {\n                Files.readAllLines(filePath).forEach(l -> {\n                    TestUtils.checkMarkdownLink(filePath.toString(), filePath, l, errors);\n                });\n            } catch (IOException e) {\n                e.printStackTrace();\n                errors.add(String.format(\"Unable to read lines from %s: %s\", filePath, e));\n            }\n        }\n    }\n\n    static List<String> checkDirectoryContents(Path directory, Tui tui,\n            BiFunction<Path, List<String>, List<String>> checker) {\n        List<String> errors = new ArrayList<>();\n\n        Path bestiary = directory.resolve(\"bestiary\");\n        Path nullDir = bestiary.resolve(\"null\");\n        Path compendium = directory.resolve(\"compendium\");\n        if (bestiary.toFile().exists() && nullDir.toFile().exists()) {\n            errors.add(String.format(\"Found null directory in bestiary: %s\", nullDir));\n        }\n        if (bestiary.toFile().exists() && compendium.toFile().exists()) {\n            errors.add(String.format(\"Found compendium directory as a peer of bestiary: %s\", compendium));\n        }\n\n        try (Stream<Path> walk = Files.list(directory)) {\n            walk.forEach(p -> {\n                if (p.toFile().isDirectory()) {\n                    System.out.println(\"📁 Checking directory: \" + p.toString().replace(PROJECT_PATH.toString(), \"\"));\n                    errors.addAll(checkDirectoryContents(p, tui, checker));\n                    return;\n                }\n                if (!p.toString().endsWith(\".md\")) {\n                    if (!p.toString().endsWith(\".png\")\n                            && !p.toString().endsWith(\".txt\")\n                            && !p.toString().endsWith(\".jpg\")\n                            && !p.toString().endsWith(\".jpeg\")\n                            && !p.toString().endsWith(\".css\")\n                            && !p.toString().endsWith(\".svg\")\n                            && !p.toString().endsWith(\".webp\")\n                            && !p.toString().endsWith(\".json\")\n                            && !p.toString().endsWith(\".yaml\")) {\n                        errors.add(String.format(\"Found file that was not markdown: %s\", p));\n                    }\n                    return;\n                }\n                try {\n                    errors.addAll(checker.apply(p, Files.readAllLines(p)));\n                } catch (IOException e) {\n                    e.printStackTrace();\n                    errors.add(String.format(\"Unable to read lines from %s: %s\", p, e));\n                }\n            });\n        } catch (IOException e) {\n            e.printStackTrace();\n            errors.add(String.format(\"Unable to parse files in directory %s: %s\", directory, e));\n        }\n        return errors;\n    }\n\n    public static String dump(LaunchResult result) {\n        return \"\\nSystem out:\\n\" + result.getOutput()\n                + \"\\nSystem err:\\n\" + result.getErrorOutput();\n    }\n\n    public static void deleteDir(Path path) {\n        if (!path.toFile().exists()) {\n            return;\n        }\n\n        try (Stream<Path> paths = Files.walk(path)) {\n            paths.sorted(Comparator.reverseOrder())\n                    .map(Path::toFile)\n                    .forEach(File::delete);\n        } catch (IOException e) {\n            throw new UncheckedIOException(e);\n        }\n        Assertions.assertFalse(path.toFile().exists());\n    }\n\n    public static List<String> getFilesFrom(Path directory) {\n        try (Stream<Path> paths = Files.walk(directory)) {\n            return paths\n                    .filter(p -> p.toFile().isFile())\n                    .map(Path::toString)\n                    .filter(s -> s.endsWith(\".json\"))\n                    .collect(Collectors.toList());\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static void readAllToolsData(Tools5eIndex index, Path toolsData, String... dirs) throws IOException {\n        // read the tools data\n        index.tui().readToolsDir(toolsData, index::importTree);\n\n        for (String dir : dirs) {\n            readOtherFiles(index, toolsData, dir);\n        }\n\n        // read extra books, etc\n        index.resolveSources(toolsData);\n    }\n\n    public static void readOtherFiles(Tools5eIndex index, Path toolsData, String more) throws IOException {\n        for (String file : TestUtils.getFilesFrom(toolsData.resolve(more))) {\n            Path fullInputPath = Path.of(file);\n            Path relativepath = toolsData.relativize(fullInputPath);\n            index.tui().readFile(Path.of(file), TtrpgConfig.getFixes(relativepath.toString()), index::importTree);\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/java/dev/ebullient/convert/Tools5eDataConvertIT.java",
    "content": "package dev.ebullient.convert;\n\nimport org.junit.jupiter.api.BeforeAll;\n\nimport io.quarkus.test.junit.main.QuarkusMainIntegrationTest;\n\n@QuarkusMainIntegrationTest\npublic class Tools5eDataConvertIT extends Tools5eDataConvertTest {\n    @BeforeAll\n    public static void setupDir() {\n        setupDir(\"test-cli-IT\");\n    }\n}\n"
  },
  {
    "path": "src/test/java/dev/ebullient/convert/Tools5eDataConvertTest.java",
    "content": "package dev.ebullient.convert;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.io.IOException;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardCopyOption;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport dev.ebullient.convert.io.Tui;\nimport io.quarkus.test.junit.main.LaunchResult;\nimport io.quarkus.test.junit.main.QuarkusMainLauncher;\nimport io.quarkus.test.junit.main.QuarkusMainTest;\n\n@QuarkusMainTest\npublic class Tools5eDataConvertTest {\n\n    static Path rootTestOutput;\n    static Tui tui;\n\n    Path testOutput;\n\n    @BeforeAll\n    public static void setupDir() {\n        setupDir(\"test-cli\");\n    }\n\n    public static void setupDir(String name) {\n        tui = new Tui();\n        tui.init(null, false, false);\n        rootTestOutput = TestUtils.OUTPUT_ROOT_5E.resolve(name);\n        rootTestOutput.toFile().mkdirs();\n\n        tui.infof(\"5eTools sources (%s): %s\",\n                TestUtils.PATH_5E_TOOLS_DATA.toFile().exists(),\n                TestUtils.PATH_5E_TOOLS_DATA);\n        tui.infof(\"5eTools images (%s): %s\",\n                TestUtils.PATH_5E_TOOLS_IMAGES.toFile().exists(),\n                TestUtils.PATH_5E_TOOLS_IMAGES);\n        tui.infof(\"5eTools homebrew (%s): %s\",\n                TestUtils.PATH_5E_HOMEBREW.toFile().exists(),\n                TestUtils.PATH_5E_HOMEBREW);\n    }\n\n    @AfterAll\n    public static void cleanup() {\n        System.out.println(\"Done.\");\n    }\n\n    @BeforeEach\n    public void setup() {\n        testOutput = null; // test should set this to something readable\n    }\n\n    @AfterEach\n    public void clear() throws IOException {\n        assertThat(testOutput).isNotNull(); // make sure test set this\n\n        Path logFile = Path.of(\"ttrpg-convert.out.txt\");\n        if (Files.exists(logFile)) {\n            String content = Files.readString(logFile, StandardCharsets.UTF_8);\n\n            Path filePath = testOutput.resolve(logFile);\n            Files.move(logFile, filePath, StandardCopyOption.REPLACE_EXISTING);\n\n            if (content.matches(\".*?Exception(\\\\s.*|$)\")) {\n                tui.errorf(\"Exception found in %s\", filePath);\n            }\n        }\n        TestUtils.cleanupReferences();\n    }\n\n    @Test\n    void testLiveData_defaultSrd(QuarkusMainLauncher launcher) {\n        testOutput = rootTestOutput.resolve(\"default-index\");\n        if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) {\n            // SRD\n            TestUtils.deleteDir(testOutput);\n\n            Tui.instance().infof(\"--- Default content ----- \");\n\n            LaunchResult result = launcher.launch(\"--log\", \"--index\",\n                    \"-o\", testOutput.toString(),\n                    TestUtils.TEST_RESOURCES.resolve(\"5e/images-remote.json\").toString(),\n                    TestUtils.TEST_RESOURCES.resolve(\"dice-roller.json\").toString(),\n                    TestUtils.PATH_5E_TOOLS_DATA.toString());\n            assertThat(result.exitCode())\n                    .withFailMessage(\"Command failed. Output:%n%s\", TestUtils.dump(result))\n                    .isEqualTo(0);\n            TestUtils.assertDirectoryContents(testOutput, tui, (p, content) -> {\n                List<String> errors = new ArrayList<>();\n                content.forEach(l -> {\n                    TestUtils.checkMarkdownLink(testOutput.toString(), p, l, errors);\n                    TestUtils.commonTests(p, l, errors);\n                });\n                return errors;\n            });\n        }\n    }\n\n    @Test\n    void testLiveData_allSources(QuarkusMainLauncher launcher) {\n        testOutput = rootTestOutput.resolve(\"all-index\");\n        if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) {\n            // All, I mean it. Really for real.. ALL.\n            TestUtils.deleteDir(testOutput);\n\n            List<String> args = new ArrayList<>(List.of(\"--log\", \"--index\",\n                    \"-c\", TestUtils.TEST_RESOURCES.resolve(\"5e/sources-images.yaml\").toString(),\n                    \"-o\", testOutput.toString(),\n                    TestUtils.PATH_5E_TOOLS_DATA.toString()));\n\n            if (TestUtils.PATH_5E_TOOLS_IMAGES.toFile().exists()) {\n                args.add(TestUtils.TEST_RESOURCES.resolve(\"5e/images-from-local.json\").toString());\n            } else {\n                args.add(TestUtils.TEST_RESOURCES.resolve(\"5e/images-remote.json\").toString());\n            }\n\n            args.addAll(TestUtils.getFilesFrom(TestUtils.PATH_5E_TOOLS_DATA.resolve(\"adventure\")));\n            args.addAll(TestUtils.getFilesFrom(TestUtils.PATH_5E_TOOLS_DATA.resolve(\"book\")));\n\n            LaunchResult result = launcher.launch(args.toArray(new String[0]));\n            assertThat(result.exitCode())\n                    .withFailMessage(\"Command failed. Output:%n%s\", TestUtils.dump(result))\n                    .isEqualTo(0);\n\n            TestUtils.assertDirectoryContents(testOutput, tui, (p, content) -> {\n                List<String> errors = new ArrayList<>();\n                content.forEach(l -> {\n                    TestUtils.checkMarkdownLink(testOutput.toString(), p, l, errors);\n                    TestUtils.commonTests(p, l, errors);\n                });\n                return errors;\n            });\n        }\n    }\n\n    @Test\n    void testLiveData_changeDefaultSources(QuarkusMainLauncher launcher) {\n        testOutput = rootTestOutput.resolve(\"change-default-sources\");\n        if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) {\n            TestUtils.deleteDir(testOutput);\n\n            LaunchResult result = launcher.launch(\"--log\", \"--index\",\n                    \"-o\", testOutput.toString(),\n                    \"-c\", TestUtils.TEST_RESOURCES.resolve(\"5e/sources-changeDefaultSources.yaml\").toString(),\n                    TestUtils.TEST_RESOURCES.resolve(\"5e/images-remote.json\").toString(),\n                    TestUtils.PATH_5E_TOOLS_DATA.toString());\n\n            assertThat(result.exitCode())\n                    .withFailMessage(\"Command failed. Output:%n%s\", TestUtils.dump(result))\n                    .isEqualTo(0);\n\n            TestUtils.assertDirectoryContents(testOutput, tui, (p, content) -> {\n                List<String> errors = new ArrayList<>();\n                content.forEach(l -> {\n                    TestUtils.checkMarkdownLink(testOutput.toString(), p, l, errors);\n                    TestUtils.commonTests(p, l, errors);\n                });\n                return errors;\n            });\n        }\n    }\n\n    @Test\n    void testLiveData_homebrew(QuarkusMainLauncher launcher) {\n        testOutput = rootTestOutput.resolve(\"homebrew\");\n        if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists() && TestUtils.PATH_5E_HOMEBREW.toFile().exists()) {\n            TestUtils.deleteDir(testOutput);\n\n            List<String> args = new ArrayList<>(List.of(\"--index\", \"--log\",\n                    \"-c\", TestUtils.TEST_RESOURCES.resolve(\"5e/sources-homebrew.json\").toString(),\n                    \"-o\", testOutput.toString(),\n                    TestUtils.PATH_5E_TOOLS_DATA.toString()));\n\n            if (TestUtils.PATH_5E_TOOLS_IMAGES.toFile().exists()) {\n                args.add(TestUtils.TEST_RESOURCES.resolve(\"5e/images-from-local.json\").toString());\n            } else {\n                args.add(TestUtils.TEST_RESOURCES.resolve(\"5e/images-remote.json\").toString());\n            }\n\n            LaunchResult result = launcher.launch(args.toArray(new String[0]));\n            assertThat(result.exitCode())\n                    .withFailMessage(\"Command failed. Output:%n%s\", TestUtils.dump(result))\n                    .isEqualTo(0);\n\n            assertThat(testOutput.resolve(\"compendium/adventures/a-diamond-in-the-rough\")).isDirectory();\n            assertThat(testOutput.resolve(\"compendium/adventures/book-of-lairs\")).isDirectory();\n            assertThat(testOutput.resolve(\"compendium/adventures/call-from-the-deep\")).isDirectory();\n            assertThat(testOutput.resolve(\"compendium/adventures/tavern-of-the-lost\")).isDirectory();\n            assertThat(testOutput.resolve(\"compendium/books/arkadia\")).isDirectory();\n            assertThat(testOutput.resolve(\"compendium/books/hamunds-herbalism-handbook\")).isDirectory();\n            assertThat(testOutput.resolve(\"compendium/books/plane-shift-amonkhet\")).isDirectory();\n\n            assertThat(testOutput.resolve(\"compendium/backgrounds/cook-variant-dndwiki-bestbackgrounds.md\"))\n                    .isRegularFile();\n            assertThat(testOutput.resolve(\"compendium/classes/alchemist-dynamo-engineer-vss.md\")).isRegularFile();\n\n            TestUtils.assertDirectoryContents(testOutput, tui, (p, content) -> {\n                List<String> errors = new ArrayList<>();\n                content.forEach(l -> {\n                    TestUtils.checkMarkdownLink(testOutput.toString(), p, l, errors);\n                    TestUtils.commonTests(p, l, errors);\n                });\n                return errors;\n            });\n        }\n    }\n\n    @Test\n    void testLiveData_5eUA(QuarkusMainLauncher launcher) {\n        testOutput = rootTestOutput.resolve(\"ua\");\n        if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists() && TestUtils.PATH_5E_HOMEBREW.toFile().exists()) {\n            TestUtils.deleteDir(testOutput);\n\n            LaunchResult result = launcher.launch(\"--log\", \"--index\",\n                    \"-c\", TestUtils.TEST_RESOURCES.resolve(\"5e/sources-ua.json\").toString(),\n                    \"-o\", testOutput.toString(),\n                    TestUtils.TEST_RESOURCES.resolve(\"5e/images-remote.json\").toString(),\n                    TestUtils.PATH_5E_TOOLS_DATA.toString(),\n                    TestUtils.PATH_5E_UA.resolve(\"collection/Unearthed Arcana - Downtime.json\").toString(),\n                    TestUtils.PATH_5E_UA.resolve(\"collection/Unearthed Arcana - Encounter Building.json\").toString(),\n                    TestUtils.PATH_5E_UA.resolve(\"collection/Unearthed Arcana - Into the Wild.json\").toString(),\n                    TestUtils.PATH_5E_UA.resolve(\"collection/Unearthed Arcana - Quick Characters.json\").toString(),\n                    TestUtils.PATH_5E_UA.resolve(\"collection/Unearthed Arcana - Traps Revisited.json\").toString(),\n                    TestUtils.PATH_5E_UA.resolve(\"collection/Unearthed Arcana - When Armies Clash.json\").toString(),\n                    TestUtils.PATH_5E_UA.resolve(\"collection/Unearthed Arcana 2022 - Character Origins.json\")\n                            .toString(),\n                    TestUtils.PATH_5E_UA.resolve(\"collection/Unearthed Arcana 2022 - Expert Classes.json\").toString(),\n                    TestUtils.PATH_5E_UA\n                            .resolve(\"collection/Unearthed Arcana 2022 - The Cleric and Revised Species.json\")\n                            .toString(),\n                    TestUtils.PATH_5E_UA.resolve(\"collection/Unearthed Arcana 2023 - Bastions and Cantrips.json\")\n                            .toString(),\n                    TestUtils.PATH_5E_UA.resolve(\"collection/Unearthed Arcana 2023 - Druid & Paladin.json\").toString(),\n                    TestUtils.PATH_5E_UA.resolve(\"collection/Unearthed Arcana 2023 - Player's Handbook Playtest 5.json\")\n                            .toString(),\n                    TestUtils.PATH_5E_UA.resolve(\"collection/Unearthed Arcana 2023 - Player's Handbook Playtest 6.json\")\n                            .toString(),\n                    TestUtils.PATH_5E_UA.resolve(\"collection/Unearthed Arcana 2023 - Player's Handbook Playtest 7.json\")\n                            .toString());\n\n            assertThat(result.exitCode())\n                    .withFailMessage(\"Command failed. Output:%n%s\", TestUtils.dump(result))\n                    .isEqualTo(0);\n            TestUtils.assertDirectoryContents(testOutput, tui, (p, content) -> {\n                List<String> errors = new ArrayList<>();\n                content.forEach(l -> {\n                    TestUtils.checkMarkdownLink(testOutput.toString(), p, l, errors);\n                    TestUtils.commonTests(p, l, errors);\n                });\n                return errors;\n            });\n        }\n    }\n\n    // --- 2014 ---\n\n    @Test\n    void testLiveData_2014_srd(QuarkusMainLauncher launcher) {\n        testOutput = rootTestOutput.resolve(\"2014-srd-index\");\n        if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) {\n            // SRD 2014\n            Tui.instance().infof(\"--- 2014 SRD ----- \");\n            TestUtils.deleteDir(testOutput);\n\n            LaunchResult result = launcher.launch(\"--log\", \"--index\",\n                    \"-o\", testOutput.toString(),\n                    \"-c\", TestUtils.TEST_RESOURCES.resolve(\"5e/sources-2014-srd.yaml\").toString(),\n                    TestUtils.TEST_RESOURCES.resolve(\"5e/images-remote.json\").toString(),\n                    TestUtils.PATH_5E_TOOLS_DATA.toString());\n            assertThat(result.exitCode())\n                    .withFailMessage(\"Command failed. Output:%n%s\", TestUtils.dump(result))\n                    .isEqualTo(0);\n            TestUtils.assertDirectoryContents(testOutput, tui, (p, content) -> {\n                List<String> errors = new ArrayList<>();\n                content.forEach(l -> {\n                    TestUtils.checkMarkdownLink(testOutput.toString(), p, l, errors);\n                    TestUtils.commonTests(p, l, errors);\n                });\n                return errors;\n            });\n        }\n    }\n\n    @Test\n    void testLiveData_2014_adventureNoPHB(QuarkusMainLauncher launcher) {\n        testOutput = rootTestOutput.resolve(\"2014-adventure-no-phb\");\n        if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) {\n            TestUtils.deleteDir(testOutput);\n\n            LaunchResult result = launcher.launch(\"--log\", \"--index\",\n                    \"-o\", testOutput.toString(),\n                    \"-c\", TestUtils.TEST_RESOURCES.resolve(\"5e/sources-2014-no-phb.yaml\").toString(),\n                    TestUtils.TEST_RESOURCES.resolve(\"5e/images-remote.json\").toString(),\n                    TestUtils.PATH_5E_TOOLS_DATA.toString());\n\n            assertThat(result.exitCode())\n                    .withFailMessage(\"Command failed. Output:%n%s\", TestUtils.dump(result))\n                    .isEqualTo(0);\n\n            List<Path> dirs = List.of(\n                    testOutput.resolve(\"compendium/adventures/lost-mine-of-phandelver\"),\n                    testOutput.resolve(\"compendium/adventures/waterdeep-dragon-heist\"),\n                    testOutput.resolve(\"compendium/books/volos-guide-to-monsters\"));\n\n            dirs.forEach(d -> {\n                assertThat(d).isDirectory();\n            });\n\n            TestUtils.assertDirectoryContents(testOutput, tui, (p, content) -> {\n                List<String> errors = new ArrayList<>();\n                content.forEach(l -> {\n                    TestUtils.checkMarkdownLink(testOutput.toString(), p, l, errors);\n                    TestUtils.commonTests(p, l, errors);\n                });\n                return errors;\n            });\n        }\n    }\n\n    @Test\n    void testLiveData_2014_bookAdventureInJson(QuarkusMainLauncher launcher) {\n        testOutput = rootTestOutput.resolve(\"2014-book-adventure\");\n        if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) {\n            TestUtils.deleteDir(testOutput);\n\n            // Make sure we find the data directory if the src dir is provided\n            LaunchResult result = launcher.launch(\"--log\", \"--index\",\n                    \"-o\", testOutput.toString(),\n                    TestUtils.TEST_RESOURCES.resolve(\"5e/images-remote.json\").toString(),\n                    TestUtils.PATH_5E_TOOLS_SRC.toString(),\n                    TestUtils.TEST_RESOURCES.resolve(\"5e/sources-2014-book-adventure.json\").toString());\n\n            assertThat(result.exitCode())\n                    .withFailMessage(\"Command failed. Output:%n%s\", TestUtils.dump(result))\n                    .isEqualTo(0);\n\n            List<Path> dirs = List.of(testOutput.resolve(\"compend ium/adventures/the-wild-beyond-the-witchlight\"),\n                    testOutput.resolve(\"compend ium/books/players-handbook-2014\"));\n\n            dirs.forEach(d -> {\n                assertThat(d).isDirectory();\n            });\n\n            List<Path> files = List.of(testOutput.resolve(\"compend ium/backgrounds/witchlight-hand-wbtw.md\"),\n                    testOutput.resolve(\"compend ium/backgrounds/folk-hero.md\"));\n\n            files.forEach(f -> {\n                assertThat(f).isRegularFile();\n            });\n\n            TestUtils.assertDirectoryContents(testOutput, tui, (p, content) -> {\n                List<String> errors = new ArrayList<>();\n                content.forEach(l -> {\n                    TestUtils.checkMarkdownLink(testOutput.toString(), p, l, errors);\n                    TestUtils.commonTests(p, l, errors);\n                    if (l.contains(\"/ru les/\")) {\n                        errors.add(\"Found '/ru les/' \" + p); // not escaped\n                    }\n                    if (l.contains(\"/compend ium/\")) {\n                        errors.add(\"Found '/compend ium/' \" + p); // not escaped\n                    }\n                });\n                return errors;\n            });\n        }\n    }\n\n    @Test\n    void testLiveData_2014_oneSource(QuarkusMainLauncher launcher) {\n        testOutput = rootTestOutput.resolve(\"erlw\");\n        if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) {\n            TestUtils.deleteDir(testOutput);\n\n            // No basics\n            LaunchResult result = launcher.launch(\"--log\", \"--index\",\n                    \"-o\", testOutput.toString(),\n                    \"-c\", TestUtils.TEST_RESOURCES.resolve(\"5e/sources-single.yaml\").toString(),\n                    TestUtils.TEST_RESOURCES.resolve(\"5e/images-remote.json\").toString(),\n                    TestUtils.PATH_5E_TOOLS_DATA.toString());\n            assertThat(result.exitCode())\n                    .withFailMessage(\"Command failed. Output:%n%s\", TestUtils.dump(result))\n                    .isEqualTo(0);\n\n            TestUtils.assertDirectoryContents(testOutput, tui, (p, content) -> {\n                List<String> errors = new ArrayList<>();\n                content.forEach(l -> {\n                    TestUtils.checkMarkdownLink(testOutput.toString(), p, l, errors);\n                    TestUtils.commonTests(p, l, errors);\n                    if (l.matches(\".*-ua[^.]\\\\.md.*$\")) {\n                        errors.add(String.format(\"Found UA resources in %s: %s\", p.toString(), l));\n                    }\n                });\n                return errors;\n            });\n        }\n    }\n\n    @Test\n    void testLiveData_2014_sample(QuarkusMainLauncher launcher) {\n        testOutput = rootTestOutput.resolve(\"sample\");\n        if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) {\n\n            TestUtils.deleteDir(testOutput);\n\n            Tui.instance().infof(\"--- Sample content ----- \");\n\n            LaunchResult result = launcher.launch(\"--log\", \"--index\",\n                    \"-o\", testOutput.toString(),\n                    \"-c\", TestUtils.TEST_RESOURCES.resolve(\"5e/sample.yaml\").toString(),\n                    TestUtils.PATH_5E_TOOLS_DATA.toString());\n            assertThat(result.exitCode())\n                    .withFailMessage(\"Command failed. Output:%n%s\", TestUtils.dump(result))\n                    .isEqualTo(0);\n        }\n    }\n\n    // --- 2024 ---\n\n    @Test\n    void testLiveData_2024_srd(QuarkusMainLauncher launcher) {\n        testOutput = rootTestOutput.resolve(\"2024-srd-index\");\n        if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) {\n            // SRD 2024\n            Tui.instance().infof(\"--- 2024 SRD ----- \");\n            TestUtils.deleteDir(testOutput);\n\n            LaunchResult result = launcher.launch(\"--log\", \"--index\",\n                    \"-o\", testOutput.toString(),\n                    \"-c\", TestUtils.TEST_RESOURCES.resolve(\"5e/sources-2024-srd.yaml\").toString(),\n                    TestUtils.TEST_RESOURCES.resolve(\"5e/images-remote.json\").toString(),\n                    TestUtils.PATH_5E_TOOLS_DATA.toString());\n\n            assertThat(result.exitCode())\n                    .withFailMessage(\"Command failed. Output:%n%s\", TestUtils.dump(result))\n                    .isEqualTo(0);\n\n            assertThat(testOutput.resolve(\"compendium/species\")).isDirectory();\n            assertThat(testOutput.resolve(\"compendium/races\")).doesNotExist();\n\n            // splitRules: individual note subdirectories exist\n            assertThat(testOutput.resolve(\"rules/conditions\")).isDirectoryContaining(\"glob:**/blinded.md\");\n            assertThat(testOutput.resolve(\"rules/actions\")).isDirectory();\n            assertThat(testOutput.resolve(\"rules/senses\")).isDirectory();\n            assertThat(testOutput.resolve(\"rules/skills\")).isDirectory();\n            assertThat(testOutput.resolve(\"rules/item-mastery\")).isDirectoryContaining(\"glob:**/cleave.md\");\n            // collated doc is a folder note inside the subdirectory\n            assertThat(testOutput.resolve(\"rules/conditions/conditions.md\")).isRegularFile();\n            assertThat(testOutput.resolve(\"rules/actions/actions.md\")).isRegularFile();\n            assertThat(testOutput.resolve(\"rules/item-mastery/item-mastery.md\")).isRegularFile();\n\n            TestUtils.assertDirectoryContents(testOutput, tui, (p, content) -> {\n                List<String> errors = new ArrayList<>();\n                content.forEach(l -> {\n                    TestUtils.checkMarkdownLink(testOutput.toString(), p, l, errors);\n                    TestUtils.commonTests(p, l, errors);\n                });\n                return errors;\n            });\n        }\n    }\n\n    @Test\n    void testLiveData_2024_subset(QuarkusMainLauncher launcher) {\n        testOutput = rootTestOutput.resolve(\"2024-subset\");\n        if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) {\n            TestUtils.deleteDir(testOutput);\n\n            LaunchResult result = launcher.launch(\"--log\", \"--index\",\n                    \"-o\", testOutput.toString(),\n                    \"-c\", TestUtils.TEST_RESOURCES.resolve(\"5e/sources-2024-subset.yaml\").toString(),\n                    TestUtils.TEST_RESOURCES.resolve(\"5e/images-remote.json\").toString(),\n                    TestUtils.PATH_5E_TOOLS_DATA.toString());\n\n            assertThat(result.exitCode())\n                    .withFailMessage(\"Command failed. Output:%n%s\", TestUtils.dump(result))\n                    .isEqualTo(0);\n\n            assertThat(testOutput.resolve(\"compendium/species\")).doesNotExist();\n            assertThat(testOutput.resolve(\"compendium/races\")).isDirectory();\n\n            TestUtils.assertDirectoryContents(testOutput, tui, (p, content) -> {\n                List<String> errors = new ArrayList<>();\n                content.forEach(l -> {\n                    TestUtils.checkMarkdownLink(testOutput.toString(), p, l, errors);\n                    TestUtils.commonTests(p, l, errors);\n                });\n                return errors;\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/java/dev/ebullient/convert/config/ConfiguratorTest.java",
    "content": "package dev.ebullient.convert.config;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\nimport java.nio.file.Path;\nimport java.util.Collection;\nimport java.util.List;\n\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\nimport dev.ebullient.convert.TestUtils;\nimport dev.ebullient.convert.config.CompendiumConfig.Configurator;\nimport dev.ebullient.convert.io.Tui;\nimport io.quarkus.arc.Arc;\nimport io.quarkus.test.junit.QuarkusTest;\n\n@QuarkusTest\npublic class ConfiguratorTest {\n    protected static Tui tui;\n\n    @BeforeAll\n    public static void prepare() {\n        tui = Arc.container().instance(Tui.class).get();\n        tui.init(null, true, false);\n    }\n\n    @Test\n    public void testPath() throws Exception {\n        Configurator test = new Configurator(tui);\n\n        tui.readFile(TestUtils.TEST_RESOURCES.resolve(\"paths.json\"), List.of(), (f, node) -> {\n            test.readConfigIfPresent(node);\n            CompendiumConfig config = TtrpgConfig.getConfig();\n            assertThat(config).isNotNull();\n            assertThat(config.compendiumVaultRoot()).isEqualTo(\"\");\n            assertThat(config.compendiumFilePath()).isEqualTo(CompendiumConfig.CWD);\n            assertThat(config.rulesVaultRoot()).isEqualTo(\"rules/\");\n            assertThat(config.rulesFilePath()).isEqualTo(Path.of(\"rules/\"));\n            assertThat(config.images.copyInternal()).isFalse();\n        });\n    }\n\n    @Test\n    public void testSources() throws Exception {\n        TtrpgConfig.init(tui, Datasource.tools5e);\n        Configurator test = new Configurator(tui);\n\n        tui.readFile(TestUtils.TEST_RESOURCES.resolve(\"5e/sources.json\"), List.of(), (f, node) -> {\n            test.readConfigIfPresent(node);\n            CompendiumConfig config = TtrpgConfig.getConfig();\n            config.resolveAdventures();\n            config.resolveBooks();\n            config.resolveHomebrew();\n\n            assertThat(config).isNotNull();\n            assertThat(config.allSources()).isFalse();\n            assertThat(config.sourceIncluded(\"phb\")).isTrue();\n            assertThat(config.sourceIncluded(\"scag\")).isFalse();\n            assertThat(config.sourceIncluded(\"dmg\")).isTrue();\n            assertThat(config.sourceIncluded(\"xge\")).isTrue();\n            assertThat(config.sourceIncluded(\"tce\")).isTrue();\n            assertThat(config.sourceIncluded(\"wbtw\")).isTrue();\n        });\n    }\n\n    @Test\n    public void testFromAll() throws Exception {\n        TtrpgConfig.init(tui, Datasource.tools5e);\n        Configurator test = new Configurator(tui);\n\n        tui.readFile(TestUtils.TEST_RESOURCES.resolve(\"sources-from-all.json\"), List.of(), (f, node) -> {\n            test.readConfigIfPresent(node);\n            CompendiumConfig config = TtrpgConfig.getConfig();\n\n            assertThat(config).isNotNull();\n            assertThat(config.allSources()).isTrue();\n            assertThat(config.sourceIncluded(\"scag\")).isTrue();\n        });\n    }\n\n    @Test\n    public void testBooksAdventures() throws Exception {\n        TtrpgConfig.init(tui, Datasource.tools5e);\n        Configurator test = new Configurator(tui);\n\n        tui.readFile(TestUtils.TEST_RESOURCES.resolve(\"5e/sources-2014-book-adventure.json\"), List.of(), (f, node) -> {\n            test.readConfigIfPresent(node);\n            CompendiumConfig config = TtrpgConfig.getConfig();\n\n            Collection<String> books = config.resolveBooks();\n            Collection<String> adventures = config.resolveAdventures();\n            Collection<String> homebrew = config.resolveHomebrew();\n\n            assertThat(config).isNotNull();\n\n            // empty values should be filtered out\n            assertThat(books).size().isEqualTo(2);\n            assertThat(adventures).size().isEqualTo(3);\n            assertThat(homebrew).size().isEqualTo(0);\n\n            assertThat(books).contains(\"book/book-phb.json\");\n            assertThat(adventures).contains(\"adventure/adventure-wbtw.json\");\n\n            assertThat(config.compendiumVaultRoot()).isEqualTo(\"/compend%20ium/\");\n            assertThat(config.compendiumFilePath()).isEqualTo(Path.of(\"compend ium/\"));\n            assertThat(config.rulesVaultRoot()).isEqualTo(\"/ru%20les/\");\n            assertThat(config.rulesFilePath()).isEqualTo(Path.of(\"ru les/\"));\n        });\n    }\n\n    @Test\n    public void testSourcesBadTemplates() throws Exception {\n        TtrpgConfig.init(tui, Datasource.tools5e);\n        Configurator test = new Configurator(tui);\n\n        tui.readFile(TestUtils.TEST_RESOURCES.resolve(\"sources-bad-template.json\"), List.of(), (f, node) -> {\n            assertThrows(IllegalArgumentException.class,\n                    () -> test.readConfigIfPresent(node));\n\n            CompendiumConfig config = TtrpgConfig.getConfig();\n            assertThat(config).isNotNull();\n            assertThat(config.getCustomTemplate(\"background\")).isNull();\n        });\n    }\n\n    @Test\n    public void testSourcesNoImages() throws Exception {\n        TtrpgConfig.init(tui, Datasource.tools5e);\n        Configurator test = new Configurator(tui);\n\n        tui.readFile(TestUtils.TEST_RESOURCES.resolve(\"5e/images-remote.json\"), List.of(), (f, node) -> {\n            test.readConfigIfPresent(node);\n            CompendiumConfig config = TtrpgConfig.getConfig();\n\n            assertThat(config).isNotNull();\n            assertThat(config.imageOptions()).isNotNull();\n            assertThat(config.imageOptions().copyInternal()).isFalse();\n        });\n    }\n}\n"
  },
  {
    "path": "src/test/java/dev/ebullient/convert/config/ConfiguratorUtil.java",
    "content": "package dev.ebullient.convert.config;\n\nimport java.nio.file.Path;\n\nimport dev.ebullient.convert.io.Tui;\n\npublic class ConfiguratorUtil {\n\n    public static CompendiumConfig testCustomTemplate(String key, Path p) {\n        CompendiumConfig base = TtrpgConfig.getConfig();\n        TemplatePaths templatePaths = new TemplatePaths();\n        templatePaths.setCustomTemplate(key, p);\n\n        return ConfiguratorUtil.copy(base, templatePaths);\n    }\n\n    public static CompendiumConfig createNewConfig(Tui tui) {\n        return createNewConfig(tui, Datasource.tools5e);\n    }\n\n    public static CompendiumConfig createNewConfig(Tui tui, Datasource datasource) {\n        TtrpgConfig.init(tui, datasource);\n        return TtrpgConfig.getConfig();\n    }\n\n    public static CompendiumConfig copy(CompendiumConfig base, TemplatePaths newTemplates) {\n        CompendiumConfig copy = new CompendiumConfig(base.datasource, base.tui);\n\n        copy.allSources = base.allSources;\n        copy.paths = base.paths;\n\n        copy.adventures.addAll(base.adventures);\n        copy.books.addAll(base.books);\n\n        copy.allowedSources.addAll(base.allowedSources);\n        copy.customTemplates.putAll(newTemplates.customTemplates);\n        copy.excludedKeys.addAll(base.excludedKeys);\n        copy.excludedPatterns.addAll(base.excludedPatterns);\n        copy.includedGroups.addAll(base.includedGroups);\n        copy.includedKeys.addAll(base.includedKeys);\n\n        return copy;\n    }\n}\n"
  },
  {
    "path": "src/test/java/dev/ebullient/convert/config/ExportDocsTest.java",
    "content": "package dev.ebullient.convert.config;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardOpenOption;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.github.victools.jsonschema.generator.OptionPreset;\nimport com.github.victools.jsonschema.generator.SchemaGenerator;\nimport com.github.victools.jsonschema.generator.SchemaGeneratorConfig;\nimport com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;\nimport com.github.victools.jsonschema.generator.SchemaVersion;\n\nimport dev.ebullient.convert.TestUtils;\nimport dev.ebullient.convert.config.TtrpgConfig.ConfigKeys;\nimport dev.ebullient.convert.config.TtrpgConfig.SourceReference;\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.tools.JsonNodeReader;\nimport io.quarkus.arc.Arc;\nimport io.quarkus.test.junit.QuarkusTest;\n\n@QuarkusTest\npublic class ExportDocsTest {\n    protected static Tui tui;\n\n    @BeforeAll\n    public static void prepare() {\n        tui = Arc.container().instance(Tui.class).get();\n        tui.init(null, true, false);\n    }\n\n    @Test\n    public void exportSourceMap() throws Exception {\n        Path in = Path.of(\"src/test/resources/sourcemap.txt\");\n        Path out = Path.of(\"docs/sourceMap.md\");\n        Path sourceTypes = Path.of(\"src/test/resources/5e-sourceTypes.json\");\n\n        final SourceTypes types;\n\n        if (TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) {\n            JsonNode adventures = Tui.MAPPER.readTree(TestUtils.PATH_5E_TOOLS_DATA.resolve(\"adventures.json\").toFile());\n            var adventureIds = AdventureList.adventure.streamFrom(adventures)\n                    .map(x -> x.get(\"id\").asText())\n                    .toList();\n\n            JsonNode books = Tui.MAPPER.readTree(TestUtils.PATH_5E_TOOLS_DATA.resolve(\"books.json\").toFile());\n            var bookIds = AdventureList.book.streamFrom(books)\n                    .map(x -> x.get(\"id\").asText())\n                    .toList();\n\n            types = new SourceTypes(adventureIds, bookIds);\n\n            // Update list\n            tui.writeJsonFile(sourceTypes, types);\n        } else {\n            types = Tui.MAPPER.readValue(sourceTypes.toFile(), SourceTypes.class);\n        }\n\n        JsonNode node = Tui.readTreeFromResource(\"/sourceMap.yaml\");\n\n        StringBuilder tools5e = new StringBuilder();\n        writeToBuilder(ConfigKeys.config5e.getFrom(node), tools5e, types, \"5eTools\");\n\n        StringBuilder toolsPf2e = new StringBuilder();\n        writeToBuilder(ConfigKeys.configPf2e.getFrom(node), toolsPf2e, null, \"Pf2eTools\");\n\n        String result = Files.readString(in)\n                .replace(\"<!--%% 5etools %% -->\\n\", tools5e.toString())\n                .replaceAll(\"<!--%% Pf2eTools %% -->\\n+\", toolsPf2e.toString());\n        Files.writeString(out, result, StandardOpenOption.CREATE);\n    }\n\n    void writeToBuilder(JsonNode configMap, StringBuilder builder, SourceTypes sourceTypes, String section) {\n        if (ConfigKeys.reference.existsIn(configMap)) {\n            if (sourceTypes == null) {\n                builder.append(\"### \" + section + \" Abbreviations to long name\\n\\n\");\n                builder.append(\"| Abbreviation | Long name |\\n\");\n                builder.append(\"|--------------|-----------|\\n\");\n            } else {\n                builder.append(\"### \" + section + \" Abbreviations to long name\\n\\n\");\n                builder.append(\"| Abbreviation | Long name | Type |\\n\");\n                builder.append(\"|--------------|-----------|-------|\\n\");\n            }\n\n            ConfigKeys.reference.getAs(configMap, TtrpgConfig.MAP_REFERENCE).entrySet()\n                    .stream().sorted((e1, e2) -> e1.getKey().compareTo(e2.getKey()))\n                    .forEach(e -> {\n                        SourceReference ref = e.getValue();\n                        builder.append(\"| \").append(e.getKey())\n                                .append(\" | \").append(ref.name);\n                        if (sourceTypes != null) {\n                            String type = \"reference\";\n                            if (sourceTypes.adventure.contains(e.getKey())) {\n                                type = \"adventure\";\n                            } else if (sourceTypes.book.contains(e.getKey())) {\n                                type = \"book\";\n                            }\n                            builder.append(\" | \").append(type);\n                        }\n                        builder.append(\" |\\n\");\n                    });\n        }\n\n        if (ConfigKeys.longToAbv.existsIn(configMap)) {\n            builder.append(\"\\n\");\n            builder.append(\"### \" + section + \" Alternate abbreviation mapping\\n\\n\");\n            builder.append(\n                    \"You may see these abbreviations referenced in source material, this is how they map to sources listed above.\\n\\n\");\n            builder.append(\"| Abbreviation | Alias     |\\n\");\n            builder.append(\"|--------------|-----------|\\n\");\n\n            ConfigKeys.longToAbv.getAsMap(configMap).entrySet()\n                    .stream().sorted((e1, e2) -> e1.getKey().compareTo(e2.getKey()))\n                    .forEach(e -> builder.append(\"| \").append(e.getKey()).append(\" | \").append(e.getValue())\n                            .append(\" |\\n\"));\n        }\n    }\n\n    @Test\n    public void exportExample() throws Exception {\n        UserConfig tools5Config = new UserConfig();\n\n        tools5Config.sources.toolsRoot = \"local/5etools/data\";\n        tools5Config.sources.reference.add(\"DMG\");\n        tools5Config.sources.book.add(\"PHB\");\n        tools5Config.sources.adventure.add(\"LMoP\");\n        tools5Config.sources.homebrew.add(\"homebrew/collection/Kobold Press; Deep Magic 14 Elemental Magic.json\");\n\n        tools5Config.paths.compendium = \"/compendium/\";\n        tools5Config.paths.rules = \"/compendium/rules/\";\n\n        tools5Config.excludePattern.add(\"race\\\\|.*\\\\|dmg\");\n        tools5Config.exclude.addAll(List.of(\n                \"monster|expert|dc\",\n                \"monster|expert|sdw\",\n                \"monster|expert|slw\"));\n        tools5Config.include.add(\"race|changeling|mpmm\");\n        tools5Config.includeGroup.add(\"familiars\");\n\n        tools5Config.template.put(\"background\", \"examples/templates/tools5e/images-background2md.txt\");\n\n        tools5Config.images.copyExternal = Boolean.TRUE;\n        tools5Config.images.copyInternal = Boolean.TRUE;\n        tools5Config.images.internalRoot = \"local/path/for/remote/images\";\n\n        tools5Config.useDiceRoller = true;\n        tools5Config.yamlStatblocks = true;\n        tools5Config.tagPrefix = \"ttrpg-cli\";\n\n        tui.writeJsonFile(Path.of(\"examples/config/config.5e.json\"), tools5Config);\n        tui.writeYamlFile(Path.of(\"examples/config/config.5e.yaml\"), tools5Config);\n\n        UserConfig pf2eConfig = new UserConfig();\n        pf2eConfig.sources.reference.add(\"CRB\");\n        pf2eConfig.sources.reference.add(\"GMG\");\n        pf2eConfig.sources.book.add(\"crb\");\n        pf2eConfig.sources.book.add(\"gmg\");\n\n        pf2eConfig.paths.compendium = \"compendium/\";\n        pf2eConfig.paths.rules = \"compendium/rules/\";\n\n        pf2eConfig.include.add(\"ability|buck|b1\");\n        pf2eConfig.exclude.add(\"background|insurgent|apg\");\n        pf2eConfig.excludePattern.add(\"background\\\\|.*\\\\|lowg\");\n        pf2eConfig.template.put(\"ability\", \"../path/to/ability2md.txt\");\n\n        pf2eConfig.tagPrefix = \"ttrpg-cli\";\n        pf2eConfig.useDiceRoller = true;\n\n        tui.writeJsonFile(Path.of(\"examples/config/config.pf2e.json\"), pf2eConfig);\n        tui.writeYamlFile(Path.of(\"examples/config/config.pf2e.yaml\"), pf2eConfig);\n    }\n\n    @Test\n    public void exportSchema() throws IOException {\n        SchemaGeneratorConfig config = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12,\n                OptionPreset.PLAIN_JSON)\n                .withObjectMapper(Tui.MAPPER)\n                .build();\n\n        SchemaGenerator generator = new SchemaGenerator(config);\n        JsonNode jsonSchema = generator.generateSchema(UserConfig.class);\n\n        Files.writeString(Path.of(\"examples/config/config.schema.json\"), jsonSchema.toPrettyString());\n    }\n\n    static class SourceTypes {\n        List<String> adventure = new ArrayList<>();\n        List<String> book = new ArrayList<>();\n\n        SourceTypes() {\n        }\n\n        SourceTypes(List<String> adventure, List<String> book) {\n            this.adventure.addAll(adventure);\n            this.book.addAll(book);\n        }\n    }\n\n    enum AdventureList implements JsonNodeReader {\n        adventure,\n        book,\n        id\n    }\n}\n"
  },
  {
    "path": "src/test/java/dev/ebullient/convert/docs/TemplateDocTest.java",
    "content": "package dev.ebullient.convert.docs;\n\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.condition.DisabledIfSystemProperty;\nimport org.junit.jupiter.api.condition.EnabledIfSystemProperty;\n\nimport dev.ebullient.convert.TestUtils;\nimport dev.ebullient.convert.io.MarkdownDoclet;\nimport dev.ebullient.convert.io.Tui;\n\npublic class TemplateDocTest {\n    protected static Tui tui;\n\n    @BeforeAll\n    public static void prepare() {\n        tui = new Tui();\n        tui.init(null, true, false);\n    }\n\n    @AfterEach\n    public void clear() {\n        TestUtils.cleanupReferences();\n    }\n\n    // Use this test in IDEs\n    @Test\n    @DisabledIfSystemProperty(named = \"maven.home\", matches = \".*\")\n    public void buildVerifyDocs() throws Exception {\n        MarkdownDoclet.main(null);\n        verifyDocs();\n    }\n\n    // Use this test in maven builds\n    @Test\n    @EnabledIfSystemProperty(named = \"maven.home\", matches = \".*\")\n    public void verifyDocs() throws Exception {\n        TestUtils.assertMarkdownLinks(TestUtils.PROJECT_PATH.resolve(\"docs\"), tui);\n        TestUtils.assertMarkdownLinks(TestUtils.PROJECT_PATH.resolve(\"examples\"), tui);\n        TestUtils.assertMarkdownLinks(TestUtils.PROJECT_PATH.resolve(\"README.md\"), tui);\n        TestUtils.assertMarkdownLinks(TestUtils.PROJECT_PATH.resolve(\"README-WINDOWS.md\"), tui);\n        TestUtils.assertMarkdownLinks(TestUtils.PROJECT_PATH.resolve(\"CHANGELOG.md\"), tui);\n    }\n}\n"
  },
  {
    "path": "src/test/java/dev/ebullient/convert/qute/ImageRefTest.java",
    "content": "package dev.ebullient.convert.qute;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.io.UnsupportedEncodingException;\nimport java.net.MalformedURLException;\nimport java.nio.file.Path;\n\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\nimport dev.ebullient.convert.config.CompendiumConfig;\nimport dev.ebullient.convert.config.Datasource;\nimport dev.ebullient.convert.config.TtrpgConfig;\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.tools.dnd5e.Tools5eIndex;\nimport dev.ebullient.convert.tools.dnd5e.Tools5eSources;\nimport io.quarkus.arc.Arc;\nimport io.quarkus.test.junit.QuarkusTest;\n\n@QuarkusTest\npublic class ImageRefTest {\n    protected static Tui tui;\n    protected static TestIndex index;\n\n    @BeforeAll\n    public static void prepare() {\n        tui = Arc.container().instance(Tui.class).get();\n        tui.init(null, true, false);\n        TtrpgConfig.init(tui, Datasource.tools5e);\n        index = new TestIndex(TtrpgConfig.getConfig());\n    }\n\n    @Test\n    public void testImgurUrl() throws MalformedURLException, UnsupportedEncodingException {\n        String input = \"https://imgur.com/lQfZ1dF.png\";\n\n        assertThat(ImageRef.Builder.escapeUrlImagePath(input))\n                .isEqualTo(\"https://i.imgur.com/lQfZ1dF.png\");\n    }\n\n    @Test\n    public void testAccentedCharacters() throws MalformedURLException, UnsupportedEncodingException {\n        String input = \"https://whatever.com/áé.png?raw=true\";\n\n        assertThat(ImageRef.Builder.escapeUrlImagePath(input))\n                .isEqualTo(\"https://whatever.com/%C3%A1%C3%A9.png?raw=true\");\n    }\n\n    @Test\n    public void testEncodedRemoteUrl() throws Exception {\n        Tools5eSources sources = Tools5eSources.findOrTemporary(\n                Tui.MAPPER.createObjectNode()\n                        .put(\"name\", \"Critter\")\n                        .put(\"source\", \"DMG\"));\n\n        ImageRef ref = sources.buildTokenImageRef(index,\n                \"https://raw.githubusercontent.com/TheGiddyLimit/homebrew/master/_img/MonsterManualExpanded3/creature/Hill%20Giant%20Warlock%20Of%20Ogrémoch.jpg\",\n                Path.of(\"something.png\"),\n                false);\n\n        assertThat(ref.url())\n                .isEqualTo(\n                        \"https://raw.githubusercontent.com/TheGiddyLimit/homebrew/master/_img/MonsterManualExpanded3/creature/Hill%20Giant%20Warlock%20Of%20Ogr%C3%A9moch.jpg\");\n\n        ref = sources.buildTokenImageRef(index,\n                \"https://raw.githubusercontent.com/TheGiddyLimit/homebrew/master/_img/MonsterManualExpanded3/creature/token/Stone%20Giant%20Warlock%20Of%20Ogrémoch%20%28Token%29.png\",\n                Path.of(\"something.png\"),\n                false);\n\n        assertThat(ref.url())\n                .isEqualTo(\n                        \"https://raw.githubusercontent.com/TheGiddyLimit/homebrew/master/_img/MonsterManualExpanded3/creature/token/Stone%20Giant%20Warlock%20Of%20Ogr%C3%A9moch%20%28Token%29.png\");\n\n        // only Remote URL (not local)\n        ref = new ImageRef.Builder()\n                .setUrl(\"https://raw.githubusercontent.com/TheGiddyLimit/homebrew/master/_img/MonsterManualExpanded3/creature/token/Stone%20Giant%20Warlock%20Of%20Ogrémoch%20%28Token%29.png\")\n                .build();\n\n        assertThat(ref.url())\n                .isEqualTo(\n                        \"https://raw.githubusercontent.com/TheGiddyLimit/homebrew/master/_img/MonsterManualExpanded3/creature/token/Stone%20Giant%20Warlock%20Of%20Ogr%C3%A9moch%20%28Token%29.png\");\n\n        // use Remote URL\n        ref = new ImageRef.Builder()\n                .setUrl(\"https://raw.githubusercontent.com/5etools-mirror-3/5etools-img/main/bestiary/tokens/MM/Giant Owl.webp#token\")\n                .build();\n\n        assertThat(ref.url())\n                .isEqualTo(\n                        \"https://raw.githubusercontent.com/5etools-mirror-3/5etools-img/main/bestiary/tokens/MM/Giant%20Owl.webp#token\");\n    }\n\n    static class TestIndex extends Tools5eIndex {\n        public TestIndex(CompendiumConfig config) {\n            super(config);\n            prepared.set(true);\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/java/dev/ebullient/convert/qute/TtrpgTemplateExtensionTest.java",
    "content": "package dev.ebullient.convert.qute;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport org.junit.jupiter.api.Test;\n\npublic class TtrpgTemplateExtensionTest {\n\n    @Test\n    public void testTitleCase() {\n        // Basic cases\n        assertThat(TtrpgTemplateExtension.capitalized(\"hello world\")).isEqualTo(\"Hello World\");\n        assertThat(TtrpgTemplateExtension.capitalized(\"**hello** world\")).isEqualTo(\"**Hello** World\");\n        assertThat(TtrpgTemplateExtension.capitalized(null)).isNull();\n        assertThat(TtrpgTemplateExtension.capitalized(\"\")).isEmpty();\n\n        // With markdown links\n        assertThat(TtrpgTemplateExtension.capitalized(\"thing 1, [thing 2](uRl), And thing 3\"))\n                .isEqualTo(\"Thing 1, [Thing 2](uRl), and Thing 3\");\n    }\n\n    @Test\n    public void testCapitalizedList() {\n        // With markdown links\n        assertThat(TtrpgTemplateExtension\n                .capitalizedList(\"thing 1, [thing 2](uRl), and thing 3; Other thing (with additional stuff)\"))\n                .isEqualTo(\"Thing 1, [Thing 2](uRl), and Thing 3; Other thing (with additional stuff)\");\n    }\n\n    @Test\n    public void testUppercaseFirst() {\n        // Basic cases\n        assertThat(TtrpgTemplateExtension.uppercaseFirst(\"hello world\")).isEqualTo(\"Hello world\");\n        assertThat(TtrpgTemplateExtension.uppercaseFirst(\"**hello** world\")).isEqualTo(\"**Hello** world\");\n        assertThat(TtrpgTemplateExtension.uppercaseFirst(null)).isNull();\n        assertThat(TtrpgTemplateExtension.uppercaseFirst(\"\")).isEmpty();\n\n        // With markdown links, and no changes to the case of middle words..\n        assertThat(TtrpgTemplateExtension.uppercaseFirst(\"thing 1, [thing 2](url), And thing 3\"))\n                .isEqualTo(\"Thing 1, [thing 2](url), And thing 3\");\n\n        assertThat(TtrpgTemplateExtension.uppercaseFirst(\"[thing 2](url), thing 1, And thing 3\"))\n                .isEqualTo(\"[Thing 2](url), thing 1, And thing 3\");\n    }\n\n    @Test\n    public void testLowercase() {\n        // Basic cases\n        assertThat(TtrpgTemplateExtension.lowercase(\"HELLO WORLD\")).isEqualTo(\"hello world\");\n        assertThat(TtrpgTemplateExtension.lowercase(null)).isNull();\n        assertThat(TtrpgTemplateExtension.lowercase(\"\")).isEmpty();\n\n        // With markdown links\n        assertThat(TtrpgTemplateExtension.lowercase(\"THING 1, [THING 2](URL), And THING 3\"))\n                .isEqualTo(\"thing 1, [thing 2](URL), and thing 3\");\n    }\n\n    @Test\n    public void testAsBonus() {\n        assertThat(TtrpgTemplateExtension.asBonus(5)).isEqualTo(\"+5\");\n        assertThat(TtrpgTemplateExtension.asBonus(-3)).isEqualTo(\"-3\");\n        assertThat(TtrpgTemplateExtension.asBonus(0)).isEqualTo(\"+0\");\n    }\n}\n"
  },
  {
    "path": "src/test/java/dev/ebullient/convert/tools/TokenizerTest.java",
    "content": "package dev.ebullient.convert.tools;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.util.List;\n\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.config.CompendiumConfig;\nimport dev.ebullient.convert.io.Tui;\nimport io.quarkus.arc.Arc;\nimport io.quarkus.test.junit.QuarkusTest;\n\n@QuarkusTest\npublic class TokenizerTest {\n    protected static Tui tui;\n    protected static JsonTextConverter<IndexType> tokenizer;\n    protected static ToolsIndex index;\n\n    @BeforeAll\n    public static void prepare() {\n        tui = Arc.container().instance(Tui.class).get();\n        tui.init(null, true, false);\n        tokenizer = new JsonTextConverter<IndexType>() {\n\n            @Override\n            public void appendToText(List<String> inner, JsonNode target, String heading) {\n                throw new UnsupportedOperationException(\"Unimplemented method 'appendToText'\");\n            }\n\n            @Override\n            public CompendiumConfig cfg() {\n                throw new UnsupportedOperationException(\"Unimplemented method 'cfg'\");\n            }\n\n            @Override\n            public String linkify(IndexType type, String s) {\n                throw new UnsupportedOperationException(\"Unimplemented method 'linkify'\");\n            }\n\n            @Override\n            public String replaceText(String s) {\n                throw new UnsupportedOperationException(\"Unimplemented method 'replaceText'\");\n            }\n\n            @Override\n            public Tui tui() {\n                throw new UnsupportedOperationException(\"Unimplemented method 'tui'\");\n            }\n\n        };\n    }\n\n    @Test\n    public void testSimpleString() {\n        String input = \"{@atk mw} {@hit 2} to hit, reach 5 ft., one target. {@h}4 ({@damage 1d8}) bludgeoning damage, or 5 ({@damage 1d10 + 1}) bludgeoning damage if used with two hands to make a melee attack.\";\n        String expected = \"%atk% %hit% to hit, reach 5 ft., one target. %h%4 (%damage%) bludgeoning damage, or 5 (%damage%) bludgeoning damage if used with two hands to make a melee attack.\";\n\n        assertThat(tokenizer.replaceTokens(input, (s, b) -> s.replaceAll(\"\\\\{@(\\\\w+).*?}\", \"%$1%\"))).isEqualTo(expected);\n    }\n\n    @Test\n    public void testNestedString() {\n        String input = \"The elder dinosaur can use its Frightful Presence. It then makes five attacks: three with its bite, {@footnote one with its stomp, and one with its tail|{@note This statblock does not have these actions available.}}. It can use its Swallow instead of a bite.\";\n        String expected = \"The elder dinosaur can use its Frightful Presence. It then makes five attacks: three with its bite, ^[one with its stomp, and one with its tail|^[This statblock does not have these actions available.]]. It can use its Swallow instead of a bite.\";\n\n        assertThat(tokenizer.replaceTokens(input, (s, b) -> s.replaceAll(\"\\\\{@\\\\w+ (.*?)}\", \"^[$1]\"))).isEqualTo(expected);\n\n        input = \"The events of {@adventure Hoard of the Dragon Queen|HotDQ} lead directly into {@i {@i The Rise of Tiamat}.} The shape of this adventure is defined by the meetings of the Council of Waterdeep, which divide the adventure into four stages.\";\n        expected = \"The events of ^[Hoard of the Dragon Queen|HotDQ] lead directly into ^[^[The Rise of Tiamat].] The shape of this adventure is defined by the meetings of the Council of Waterdeep, which divide the adventure into four stages.\";\n\n        assertThat(tokenizer.replaceTokens(input, (s, b) -> s.replaceAll(\"\\\\{@\\\\w+ (.*?)}\", \"^[$1]\"))).isEqualTo(expected);\n    }\n}\n"
  },
  {
    "path": "src/test/java/dev/ebullient/convert/tools/dnd5e/CommonDataTests.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardCopyOption;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Stream;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.TestUtils;\nimport dev.ebullient.convert.config.CompendiumConfig;\nimport dev.ebullient.convert.config.CompendiumConfig.Configurator;\nimport dev.ebullient.convert.config.CompendiumConfig.DiceRoller;\nimport dev.ebullient.convert.config.ConfiguratorUtil;\nimport dev.ebullient.convert.config.Datasource;\nimport dev.ebullient.convert.config.TtrpgConfig;\nimport dev.ebullient.convert.io.MarkdownWriter;\nimport dev.ebullient.convert.io.Templates;\nimport dev.ebullient.convert.io.Tui;\nimport io.quarkus.arc.Arc;\n\npublic class CommonDataTests {\n    protected final Tui tui;\n    protected final Configurator configurator;\n    protected final Templates templates;\n    protected final Path toolsData;\n\n    public final boolean dataPresent;\n\n    public final Tools5eIndex index;\n    public final TestInput variant;\n    public final CompendiumConfig config;\n\n    enum TestInput {\n        all,\n        allNewest,\n        none,\n        srdEdition,\n        srd2014,\n        srd2024,\n        subset2014,\n        subset2024,\n        subsetMixed,\n        ;\n    }\n\n    public CommonDataTests(TestInput variant, String config, Path toolsData) throws Exception {\n        this.toolsData = toolsData;\n        dataPresent = toolsData.toFile().exists();\n\n        this.variant = variant;\n\n        tui = Arc.container().instance(Tui.class).get();\n        tui.init(null, !TestUtils.USING_MAVEN, true, true);\n\n        templates = Arc.container().instance(Templates.class).get();\n        tui.setTemplates(templates);\n\n        TtrpgConfig.init(tui, Datasource.tools5e);\n        TtrpgConfig.setToolsPath(TestUtils.PATH_5E_TOOLS_DATA);\n\n        configurator = new Configurator(tui);\n        configurator.readConfiguration(TestUtils.TEST_RESOURCES.resolve(\"5e/images-remote.json\"));\n\n        index = new Tools5eIndex(TtrpgConfig.getConfig());\n        Tools5eLinkifier.instance().reset();\n\n        if (dataPresent) {\n            templates.setCustomTemplates(TtrpgConfig.getConfig());\n\n            JsonNode configNode = Tui.MAPPER.readTree(config);\n            configurator.readConfigIfPresent(configNode);\n\n            // var additional = List.of(\n            //         \"adventures.json\",\n            //         \"books.json\");\n\n            // for (String x : additional) {\n            //     tui.readFile(toolsData.resolve(x), TtrpgConfig.getFixes(x), index::importTree);\n            // }\n            tui.readToolsDir(toolsData, index::importTree);\n            index.resolveSources(toolsData);\n            index.prepare();\n        }\n        this.config = TtrpgConfig.getConfig();\n    }\n\n    public void afterEach() throws Exception {\n        configurator.setUseDiceRoller(DiceRoller.disabled);\n        templates.setCustomTemplates(TtrpgConfig.getConfig());\n        TestUtils.cleanupReferences();\n    }\n\n    public void afterAll(Path outputPath) throws IOException {\n        index.cleanup();\n\n        assertThat(Tools5eIndex.instance()).isNull();\n        tui.close();\n        Path logFile = Path.of(\"ttrpg-convert.out.txt\");\n        if (Files.exists(logFile)) {\n            Path newFile = outputPath.resolve(logFile);\n            Files.move(logFile, newFile, StandardCopyOption.REPLACE_EXISTING);\n        }\n        System.out.println(\"Done.\");\n    }\n\n    public void testKeyIndex(Path outputPath) throws Exception {\n        tui.setOutputPath(outputPath);\n        if (dataPresent) {\n            Path p1Full = outputPath.resolve(\"allIndex.json\");\n            index.writeFullIndex(p1Full);\n\n            Path p1Source = outputPath.resolve(\"allSourceIndex.json\");\n            index.writeFilteredIndex(p1Source);\n\n            assertThat(p1Full).exists();\n            assertThat(p1Source).exists();\n        }\n    }\n\n    public void testAdventures(Path outputPath) {\n        tui.setOutputPath(outputPath);\n        if (dataPresent) {\n            Path testDir = deleteDir(Tools5eIndexType.adventureData, outputPath, index.compendiumFilePath());\n\n            MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui);\n            index.markdownConverter(writer)\n                    .writeFiles(Tools5eIndexType.adventureData);\n\n            TestUtils.assertDirectoryContents(testDir, tui);\n        }\n    }\n\n    public void testBackgroundList(Path outputPath) {\n        tui.setOutputPath(outputPath);\n        if (dataPresent) {\n            Path backgroundDir = deleteDir(Tools5eIndexType.background, outputPath, index.compendiumFilePath());\n\n            MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui);\n            index.markdownConverter(writer)\n                    .writeFiles(Tools5eIndexType.background);\n\n            TestUtils.assertDirectoryContents(backgroundDir, tui);\n        }\n    }\n\n    public void testBookList(Path outputPath) {\n        tui.setOutputPath(outputPath);\n        if (dataPresent) {\n            Path testDir = deleteDir(Tools5eIndexType.bookData, outputPath, index.compendiumFilePath());\n\n            MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui);\n            index.markdownConverter(writer)\n                    .writeFiles(Tools5eIndexType.bookData);\n\n            TestUtils.assertDirectoryContents(testDir, tui);\n        }\n    }\n\n    public void testClassList(Path outputPath) {\n        tui.setOutputPath(outputPath);\n\n        if (dataPresent) {\n            Path classDir = deleteDir(Tools5eIndexType.classtype, outputPath, index.compendiumFilePath());\n\n            MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui);\n            index.markdownConverter(writer)\n                    .writeFiles(Tools5eIndexType.classtype);\n\n            TestUtils.assertDirectoryContents(classDir, tui, (p, content) -> {\n                List<String> e = new ArrayList<>();\n                boolean found = false;\n                boolean index = false;\n\n                for (String l : content) {\n                    if (l.startsWith(\"# Index \")) {\n                        index = true;\n                    } else if (l.startsWith(\"## \")) {\n                        found = true; // Found class features\n                    }\n                    TestUtils.commonTests(p, l, e);\n                }\n\n                if (!found && !index) {\n                    e.add(String.format(\"File %s did not contain class features\", p));\n                }\n                return e;\n            });\n        }\n    }\n\n    public void testDeckList(Path outputPath) {\n        tui.setOutputPath(outputPath);\n\n        if (dataPresent) {\n            Path outDir = deleteDir(Tools5eIndexType.deck, outputPath, index.compendiumFilePath());\n\n            MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui);\n            index.markdownConverter(writer)\n                    .writeFiles(Tools5eIndexType.deck);\n\n            TestUtils.assertDirectoryContents(outDir, tui);\n        }\n    }\n\n    public void testDeityList(Path outputPath) {\n        tui.setOutputPath(outputPath);\n\n        if (dataPresent) {\n            Path outDir = deleteDir(Tools5eIndexType.deity, outputPath, index.compendiumFilePath());\n\n            MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui);\n            index.markdownConverter(writer)\n                    .writeFiles(Tools5eIndexType.deity);\n\n            TestUtils.assertDirectoryContents(outDir, tui);\n        }\n    }\n\n    public void testFacilityList(Path outputPath) {\n        tui.setOutputPath(outputPath);\n        if (dataPresent) {\n            Path outDir = deleteDir(Tools5eIndexType.facility, outputPath, index.compendiumFilePath());\n\n            MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui);\n            index.markdownConverter(writer)\n                    .writeFiles(Tools5eIndexType.facility);\n\n            TestUtils.assertDirectoryContents(outDir, tui);\n        }\n    }\n\n    public void testFeatList(Path outputPath) {\n        tui.setOutputPath(outputPath);\n        if (dataPresent) {\n            Path featDir = deleteDir(Tools5eIndexType.feat, outputPath, index.compendiumFilePath());\n\n            MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui);\n            index.markdownConverter(writer)\n                    .writeFiles(Tools5eIndexType.feat);\n\n            TestUtils.assertDirectoryContents(featDir, tui);\n        }\n    }\n\n    public void testItemList(Path outputPath) {\n        tui.setOutputPath(outputPath);\n\n        if (dataPresent) {\n            Path itemDir = deleteDir(Tools5eIndexType.item, outputPath, index.compendiumFilePath());\n\n            MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui);\n            index.markdownConverter(writer)\n                    .writeFiles(List.of(Tools5eIndexType.item, Tools5eIndexType.itemGroup));\n\n            TestUtils.assertDirectoryContents(itemDir, tui);\n        }\n    }\n\n    public void testMonsterList(Path outputPath) {\n        tui.setOutputPath(outputPath);\n        configurator.setUseDiceRoller(DiceRoller.disabled);\n\n        if (dataPresent) {\n            Path bestiaryDir = deleteDir(Tools5eIndexType.monster, outputPath, index.compendiumFilePath());\n\n            MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui);\n            index.markdownConverter(writer)\n                    .writeFiles(List.of(Tools5eIndexType.monster, Tools5eIndexType.legendaryGroup));\n\n            try (Stream<Path> paths = Files.list(bestiaryDir)) {\n                paths.forEach(p -> {\n                    if (p.toFile().isDirectory()) {\n                        TestUtils.assertDirectoryContents(p, tui);\n                    }\n                });\n            } catch (IOException e) {\n                e.printStackTrace();\n            }\n        }\n    }\n\n    public void testMonsterAlternateScores(Path outputPath) {\n        if (dataPresent) {\n            Path out = outputPath.resolve(\"alt-scores\");\n            TestUtils.deleteDir(out);\n            tui.setOutputPath(out);\n\n            CompendiumConfig testConfig = ConfiguratorUtil.testCustomTemplate(\"monster\",\n                    TestUtils.PROJECT_PATH.resolve(\"examples/templates/tools5e/monster2md-scores.txt\"));\n            templates.setCustomTemplates(testConfig);\n\n            MarkdownWriter writer = new MarkdownWriter(out, templates, tui);\n            index.markdownConverter(writer)\n                    .writeFiles(List.of(Tools5eIndexType.monster, Tools5eIndexType.legendaryGroup));\n        }\n    }\n\n    public void testMonsterYamlHeader(Path outputPath) {\n        if (dataPresent) {\n            Path out = outputPath.resolve(\"yaml-header\");\n            TestUtils.deleteDir(out);\n            tui.setOutputPath(out);\n            configurator.setUseDiceRoller(DiceRoller.enabled);\n\n            CompendiumConfig testConfig = ConfiguratorUtil.testCustomTemplate(\"monster\",\n                    TestUtils.PROJECT_PATH.resolve(\"examples/templates/tools5e/monster2md-yamlStatblock-header.txt\"));\n            templates.setCustomTemplates(testConfig);\n\n            MarkdownWriter writer = new MarkdownWriter(out, templates, tui);\n            index.markdownConverter(writer)\n                    .writeFiles(List.of(Tools5eIndexType.monster, Tools5eIndexType.legendaryGroup));\n\n            Path undead = out.resolve(index.compendiumFilePath()).resolve(linkifier().monsterPath(false, \"undead\"));\n            assertThat(undead).exists();\n\n            TestUtils.assertDirectoryContents(undead, tui, (p, content) -> {\n                List<String> errors = new ArrayList<>();\n                boolean found = false;\n                boolean index = false;\n                List<String> frontmatter = new ArrayList<>();\n\n                if (!content.get(0).equals(\"---\")) {\n                    errors.add(String.format(\"File %s did not contain frontmatter\", p));\n                    return errors;\n                }\n\n                for (String l : content.subList(1, content.size())) {\n                    if (l.startsWith(\"cssclasses:\")) {\n                        index = true;\n                    } else if (l.equals(\"statblock: true\")) {\n                        found = true;\n                    }\n                    if (l.equals(\"---\")) {\n                        break;\n                    } else {\n                        frontmatter.add(l);\n                        TestUtils.commonTests(p, l, errors);\n                    }\n                }\n\n                try {\n                    String yamlContent = String.join(\"\\n\", frontmatter);\n                    Tui.quotedYaml().load(yamlContent);\n                    if (yamlContent.contains(\"Allip\") && yamlContent.matches(\"\\\"Allip \\\\(.*?\\\\)\\\"\")) {\n                        errors.add(String.format(\"resource.5eInitiativeYamlNoSource contains source with the name\", p));\n                    }\n                } catch (Exception e) {\n                    errors.add(String.format(\"File %s contains invalid yaml: %s\", p, e));\n                }\n                if (!found && !index) {\n                    errors.add(String.format(\"File %s did not contain a statblock in the frontmatter\", p));\n                }\n                return errors;\n            });\n        }\n    }\n\n    public void testMonsterYamlBody(Path outputPath) {\n        if (dataPresent) {\n            Path out = outputPath.resolve(\"yaml-body\");\n            TestUtils.deleteDir(out);\n            tui.setOutputPath(out);\n            configurator.setUseDiceRoller(DiceRoller.enabledUsingFS);\n\n            CompendiumConfig testConfig = ConfiguratorUtil.testCustomTemplate(\"monster\",\n                    TestUtils.PROJECT_PATH.resolve(\"examples/templates/tools5e/monster2md-yamlStatblock-body.txt\"));\n            templates.setCustomTemplates(testConfig);\n\n            MarkdownWriter writer = new MarkdownWriter(out, templates, tui);\n            index.markdownConverter(writer)\n                    .writeFiles(List.of(Tools5eIndexType.monster, Tools5eIndexType.legendaryGroup));\n\n            Path undead = out.resolve(index.compendiumFilePath()).resolve(linkifier().monsterPath(false, \"undead\"));\n            assertThat(undead).exists();\n\n            TestUtils.assertDirectoryContents(undead, tui, (p, content) -> {\n                List<String> errors = new ArrayList<>();\n                boolean found = false;\n                boolean yaml = false;\n                boolean index = false;\n                List<String> statblock = new ArrayList<>();\n\n                for (String l : content) {\n                    if (l.startsWith(\"# Index \")) {\n                        index = true;\n                    } else if (l.equals(\"```statblock\")) {\n                        found = yaml = true; // start yaml block\n                    } else if (l.equals(\"```\")) {\n                        yaml = false; // end yaml block\n                    } else if (yaml) {\n                        statblock.add(l);\n                        if (l.contains(\"\\\"desc\\\": \\\"\\\"\")) {\n                            errors.add(String.format(\"Found empty description in %s: %s\", p, l));\n                        }\n                    }\n                    TestUtils.commonTests(p, l, errors);\n                }\n\n                try {\n                    String yamlContent = String.join(\"\\n\", statblock);\n                    Tui.quotedYaml().load(yamlContent);\n                    if (yamlContent.contains(\"Allip\") && yamlContent.contains(\"\\\"Allip\\\"\")) {\n                        errors.add(String.format(\"resource.5eStatblockYaml does not contain source with the name\", p));\n                    }\n                } catch (Exception e) {\n                    errors.add(String.format(\"File %s contains invalid yaml: %s\", p, e));\n                }\n                if (!found && !index) {\n                    errors.add(String.format(\"File %s did not contain a yaml statblock\", p));\n                }\n                return errors;\n            });\n        }\n    }\n\n    public void testMonster2024(Path outputPath) {\n        if (dataPresent) {\n            Path out = outputPath.resolve(\"monster-2024\");\n            TestUtils.deleteDir(out);\n            tui.setOutputPath(out);\n            configurator.setUseDiceRoller(DiceRoller.enabledUsingFS);\n\n            CompendiumConfig testConfig = ConfiguratorUtil.testCustomTemplate(\"monster\",\n                    TestUtils.PROJECT_PATH.resolve(\"examples/templates/tools5e/monster2md-2024.txt\"));\n            templates.setCustomTemplates(testConfig);\n\n            MarkdownWriter writer = new MarkdownWriter(out, templates, tui);\n            index.markdownConverter(writer)\n                    .writeFiles(List.of(Tools5eIndexType.monster, Tools5eIndexType.legendaryGroup));\n\n            TestUtils.assertDirectoryContents(out, tui);\n        }\n    }\n\n    public void testObjectList(Path outputPath) {\n        tui.setOutputPath(outputPath);\n\n        if (dataPresent) {\n            Path outDir = deleteDir(Tools5eIndexType.object, outputPath, index.compendiumFilePath());\n\n            MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui);\n            index.markdownConverter(writer)\n                    .writeFiles(List.of(Tools5eIndexType.object));\n\n            TestUtils.assertDirectoryContents(outDir, tui);\n        }\n    }\n\n    public void testOptionalFeatureList(Path outputPath) {\n        tui.setOutputPath(outputPath);\n\n        if (dataPresent) {\n            Path ofDir = deleteDir(Tools5eIndexType.optionalFeatureTypes, outputPath, index.compendiumFilePath());\n\n            MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui);\n            index.markdownConverter(writer)\n                    .writeFiles(List.of(\n                            Tools5eIndexType.optionalFeatureTypes,\n                            Tools5eIndexType.optfeature));\n\n            TestUtils.assertDirectoryContents(ofDir, tui);\n        }\n    }\n\n    public void testPsionicList(Path outputPath) {\n        tui.setOutputPath(outputPath);\n\n        if (dataPresent) {\n            Path outDir = deleteDir(Tools5eIndexType.psionic, outputPath, index.compendiumFilePath());\n\n            MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui);\n            index.markdownConverter(writer)\n                    .writeFiles(Tools5eIndexType.psionic);\n\n            TestUtils.assertDirectoryContents(outDir, tui);\n        }\n    }\n\n    public void testRaceList(Path outputPath) {\n        tui.setOutputPath(outputPath);\n\n        if (dataPresent) {\n            Path raceDir = deleteDir(Tools5eIndexType.race, outputPath, index.compendiumFilePath());\n\n            MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui);\n            index.markdownConverter(writer)\n                    .writeFiles(Tools5eIndexType.race);\n\n            TestUtils.assertDirectoryContents(raceDir, tui);\n        }\n    }\n\n    public void testRewardList(Path outputPath) {\n        tui.setOutputPath(outputPath);\n\n        if (dataPresent) {\n            Path rewardDir = deleteDir(Tools5eIndexType.reward, outputPath, index.compendiumFilePath());\n\n            MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui);\n            index.markdownConverter(writer)\n                    .writeFiles(Tools5eIndexType.reward);\n\n            if (rewardDir.toFile().exists()) {\n                TestUtils.assertDirectoryContents(rewardDir, tui);\n            }\n        }\n    }\n\n    public void testRules(Path outputPath) {\n        tui.setOutputPath(outputPath);\n\n        if (dataPresent) {\n            deleteDir(Tools5eIndexType.adventureData, outputPath, index.compendiumFilePath());\n            deleteDir(Tools5eIndexType.bookData, outputPath, index.compendiumFilePath());\n            deleteDir(Tools5eIndexType.table, outputPath, index.compendiumFilePath());\n            TestUtils.deleteDir(index.rulesFilePath());\n\n            MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui);\n            index.markdownConverter(writer).writeFiles(Stream.of(Tools5eIndexType.values())\n                    .filter(x -> (x.isOutputType() && !x.useCompendiumBase())\n                            || x == Tools5eIndexType.table\n                            || x == Tools5eIndexType.bookData\n                            || x == Tools5eIndexType.adventureData)\n                    .toList());\n\n            TestUtils.assertDirectoryContents(outputPath.resolve(index.rulesFilePath()), tui);\n        }\n    }\n\n    public void testSpellList(Path outputPath) {\n        tui.setOutputPath(outputPath);\n\n        if (dataPresent) {\n            Path spellDir = deleteDir(Tools5eIndexType.spell, outputPath, index.compendiumFilePath());\n\n            MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui);\n            index.markdownConverter(writer)\n                    .writeFiles(List.of(Tools5eIndexType.spell, Tools5eIndexType.spellIndex));\n\n            TestUtils.assertDirectoryContents(spellDir, tui);\n        }\n    }\n\n    public void testTrapsHazardsList(Path outputPath) {\n        tui.setOutputPath(outputPath);\n\n        if (dataPresent) {\n            Path trapsDir = deleteDir(Tools5eIndexType.trap, outputPath, index.compendiumFilePath());\n\n            MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui);\n            index.markdownConverter(writer)\n                    .writeFiles(List.of(Tools5eIndexType.trap, Tools5eIndexType.hazard));\n\n            TestUtils.assertDirectoryContents(trapsDir, tui);\n        }\n    }\n\n    public void testVehicleList(Path outputPath) {\n        tui.setOutputPath(outputPath);\n\n        if (dataPresent) {\n            Path outDir = deleteDir(Tools5eIndexType.vehicle, outputPath, index.compendiumFilePath());\n\n            MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui);\n            index.markdownConverter(writer)\n                    .writeFiles(List.of(Tools5eIndexType.vehicle));\n\n            if (outDir.toFile().exists()) {\n                TestUtils.assertDirectoryContents(outDir, tui);\n            }\n        }\n    }\n\n    public Path compendiumFilePath() {\n        return index.compendiumFilePath();\n    }\n\n    Path deleteDir(Tools5eIndexType type, Path outputPath, Path vaultPath) {\n        final String relative = linkifier().getRelativePath(type);\n        final Path typeDir = outputPath.resolve(vaultPath).resolve(relative).normalize();\n        TestUtils.deleteDir(typeDir);\n        return typeDir;\n    }\n\n    public void assert_Present(String key) {\n        assertOrigin(key);\n        assertThat(index.getNode(key))\n                .describedAs(variant.name() + \" should contain \" + key)\n                .isNotNull();\n    }\n\n    public void assert_MISSING(String key) {\n        assertOrigin(key);\n        assertThat(index.getNode(key))\n                .describedAs(variant.name() + \" should not contain \" + key)\n                .isNull();\n    }\n\n    public void assertOrigin(String key) {\n        if (key.contains(\"level spell\")) {\n            key = key.replaceAll(\" \\\\(.*?level spell\\\\)\", \"\").trim();\n        }\n        assertThat(index.getOrigin(key))\n                .describedAs(\"Origin should contain \" + key)\n                .isNotNull();\n    }\n\n    private static Tools5eLinkifier linkifier() {\n        return Tools5eLinkifier.instance();\n    }\n}\n"
  },
  {
    "path": "src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllNewestTest.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.io.IOException;\nimport java.nio.file.Path;\n\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\nimport dev.ebullient.convert.TestUtils;\nimport dev.ebullient.convert.tools.dnd5e.CommonDataTests.TestInput;\nimport io.quarkus.test.junit.QuarkusTest;\n\n@QuarkusTest\npublic class FilterAllNewestTest {\n\n    static CommonDataTests commonTests;\n    static final TestInput testInput = TestInput.allNewest;\n    static final Path outputPath = TestUtils.OUTPUT_5E_DATA.resolve(testInput.name());\n\n    @BeforeAll\n    public static void setupDir() throws Exception {\n        outputPath.toFile().mkdirs();\n        String config = \"\"\"\n                {\n                    \"sources\": {\n                        \"reference\": [\n                            \"*\"\n                        ]\n                    },\n                    \"images\": {\n                        \"copyInternal\": false\n                    }\n                }\n                \"\"\".stripIndent();\n        commonTests = new CommonDataTests(testInput, config, TestUtils.PATH_5E_TOOLS_DATA);\n    }\n\n    @AfterAll\n    public static void done() throws IOException {\n        commonTests.afterAll(outputPath);\n    }\n\n    @AfterEach\n    public void cleanup() throws Exception {\n        commonTests.afterEach();\n    }\n\n    @Test\n    public void testKeyIndex() throws Exception {\n        commonTests.testKeyIndex(outputPath);\n\n        // All sources, but things that have been reprinted will be replaced by the newest version\n        // e.g. PHB elements should be missing/replaced by XPHB equivalents\n        if (commonTests.dataPresent) {\n            var config = commonTests.config;\n\n            assertThat(config.sourceIncluded(\"srd\")).isTrue();\n            assertThat(config.sourceIncluded(\"basicrules\")).isTrue();\n            assertThat(config.sourceIncluded(\"srd52\")).isTrue();\n            assertThat(config.sourceIncluded(\"basicRules2024\")).isTrue();\n\n            assertThat(config.sourceIncluded(\"DMG\")).isTrue();\n            assertThat(config.sourceIncluded(\"PHB\")).isTrue();\n\n            assertThat(config.sourceIncluded(\"XDMG\")).isTrue();\n            assertThat(config.sourceIncluded(\"XPHB\")).isTrue();\n\n            commonTests.assert_MISSING(\"action|attack|phb\");\n            commonTests.assert_Present(\"action|attack|xphb\");\n            commonTests.assert_MISSING(\"action|cast a spell|phb\");\n            commonTests.assert_MISSING(\"action|disengage|phb\");\n            commonTests.assert_Present(\"action|disengage|xphb\");\n\n            commonTests.assert_MISSING(\"feat|alert|phb\");\n            commonTests.assert_Present(\"feat|alert|xphb\");\n            commonTests.assert_Present(\"feat|dueling|xphb\");\n            commonTests.assert_MISSING(\"feat|grappler|phb\");\n            commonTests.assert_Present(\"feat|grappler|xphb\");\n            commonTests.assert_MISSING(\"feat|mobile|phb\");\n            commonTests.assert_MISSING(\"feat|moderately armored|phb\");\n            commonTests.assert_Present(\"feat|moderately armored|xphb\");\n\n            commonTests.assert_Present(\"variantrule|facing|dmg\");\n            commonTests.assert_MISSING(\"variantrule|falling|xge\");\n            commonTests.assert_Present(\"variantrule|familiars|mm\");\n            commonTests.assert_MISSING(\"variantrule|simultaneous effects|xge\");\n            commonTests.assert_Present(\"variantrule|simultaneous effects|xphb\");\n\n            commonTests.assert_MISSING(\"background|sage|phb\");\n            commonTests.assert_Present(\"background|sage|xphb\");\n            commonTests.assert_Present(\"background|baldur's gate acolyte|bgdia\");\n\n            commonTests.assert_MISSING(\"condition|blinded|phb\");\n            commonTests.assert_Present(\"condition|blinded|xphb\");\n\n            commonTests.assert_Present(\"deity|auril|faerûnian|frhof\"); // frhof supercedes scag\n            commonTests.assert_MISSING(\"deity|auril|faerûnian|scag\");\n            commonTests.assert_Present(\"deity|auril|forgotten realms|phb\");\n            commonTests.assert_Present(\"deity|chemosh|dragonlance|dsotdq\");\n            commonTests.assert_MISSING(\"deity|chemosh|dragonlance|phb\");\n            commonTests.assert_MISSING(\"deity|ehlonna|greyhawk|phb\");\n            commonTests.assert_Present(\"deity|ehlonna|greyhawk|xdmg\");\n            commonTests.assert_Present(\"deity|gruumsh|dawn war|dmg\"); // different pantheon\n            commonTests.assert_Present(\"deity|gruumsh|exandria|egw\"); // different pantheon\n            commonTests.assert_Present(\"deity|gruumsh|nonhuman|phb\"); // superseded\n            commonTests.assert_MISSING(\"deity|gruumsh|orc|scag\"); // superseded\n            commonTests.assert_Present(\"deity|gruumsh|orc|vgm\"); // keep this one\n            commonTests.assert_Present(\"deity|the traveler|eberron|erlw\");\n            commonTests.assert_MISSING(\"deity|the traveler|eberron|phb\");\n            commonTests.assert_Present(\"deity|the traveler|exandria|egw\");\n\n            commonTests.assert_MISSING(\"disease|cackle fever|dmg\");\n            commonTests.assert_Present(\"disease|cackle fever|xdmg\");\n\n            commonTests.assert_Present(\"hazard|quicksand pit|xdmg\");\n            commonTests.assert_MISSING(\"hazard|quicksand|dmg\");\n            commonTests.assert_MISSING(\"hazard|razorvine|dmg\");\n            commonTests.assert_Present(\"hazard|razorvine|xdmg\");\n\n            commonTests.assert_MISSING(\"itemgroup|arcane focus|phb\");\n            commonTests.assert_Present(\"itemgroup|arcane focus|xphb\");\n            commonTests.assert_MISSING(\"itemgroup|carpet of flying|dmg\");\n            commonTests.assert_Present(\"itemgroup|carpet of flying|xdmg\");\n            commonTests.assert_MISSING(\"itemgroup|ioun stone|dmg\");\n            commonTests.assert_MISSING(\"itemgroup|ioun stone|llk\");\n            commonTests.assert_Present(\"itemgroup|ioun stone|xdmg\");\n            commonTests.assert_MISSING(\"itemgroup|musical instrument|phb\");\n            commonTests.assert_MISSING(\"itemgroup|musical instrument|scag\");\n            commonTests.assert_Present(\"itemgroup|musical instrument|xphb\");\n            commonTests.assert_MISSING(\"itemgroup|spell scroll|dmg\");\n            commonTests.assert_Present(\"itemgroup|spell scroll|xdmg\");\n\n            commonTests.assert_MISSING(\"itemproperty|2h|phb\");\n            commonTests.assert_Present(\"itemproperty|2h|xphb\");\n            commonTests.assert_MISSING(\"itemproperty|bf|dmg\");\n            commonTests.assert_Present(\"itemproperty|bf|xdmg\");\n            commonTests.assert_Present(\"itemproperty|s|phb\");\n\n            commonTests.assert_MISSING(\"itemtype|$c|phb\");\n            commonTests.assert_Present(\"itemtype|$c|xphb\");\n            commonTests.assert_MISSING(\"itemtype|$g|dmg\");\n            commonTests.assert_Present(\"itemtype|$g|xdmg\");\n            commonTests.assert_MISSING(\"itemtype|sc|dmg\");\n            commonTests.assert_Present(\"itemtype|sc|xphb\");\n            commonTests.assert_Present(\"itemtypeadditionalentries|gs|phb|xge\");\n\n            commonTests.assert_MISSING(\"item|+1 rod of the pact keeper|dmg\");\n            commonTests.assert_Present(\"item|+1 rod of the pact keeper|xdmg\");\n            commonTests.assert_Present(\"item|+2 wraps of unarmed power|xdmg\");\n            commonTests.assert_MISSING(\"item|+2 wraps of unarmed prowess|bmt\");\n            commonTests.assert_MISSING(\"item|acid (vial)|phb\");\n            commonTests.assert_Present(\"item|acid absorbing tattoo|tce\");\n            commonTests.assert_Present(\"item|acid|xphb\");\n            commonTests.assert_Present(\"item|alchemist's doom|scc\");\n            commonTests.assert_MISSING(\"item|alchemist's fire (flask)|phb\");\n            commonTests.assert_Present(\"item|alchemist's fire|xphb\");\n            commonTests.assert_MISSING(\"item|alchemist's supplies|phb\");\n            commonTests.assert_Present(\"item|alchemist's supplies|xphb\");\n            commonTests.assert_MISSING(\"item|amulet of health|dmg\");\n            commonTests.assert_Present(\"item|amulet of health|xdmg\");\n            commonTests.assert_MISSING(\"item|amulet of proof against detection and location|dmg\");\n            commonTests.assert_Present(\"item|amulet of proof against detection and location|xdmg\");\n            commonTests.assert_MISSING(\"item|armor of invulnerability|dmg\");\n            commonTests.assert_Present(\"item|armor of invulnerability|xdmg\");\n            commonTests.assert_MISSING(\"item|automatic pistol|dmg\");\n            commonTests.assert_MISSING(\"item|automatic rifle|dmg\");\n            commonTests.assert_Present(\"item|automatic rifle|xdmg\");\n            commonTests.assert_MISSING(\"item|ball bearings (bag of 1,000)|phb\");\n            commonTests.assert_Present(\"item|ball bearings|xphb\");\n            commonTests.assert_Present(\"item|ball bearing|phb\");\n            commonTests.assert_MISSING(\"item|chain (10 feet)|phb\");\n            commonTests.assert_Present(\"item|chain|xphb\");\n\n            commonTests.assert_Present(\"monster|abjurer wizard|mpmm\");\n            commonTests.assert_MISSING(\"monster|abjurer|vgm\");\n            commonTests.assert_Present(\"monster|alkilith|mpmm\");\n            commonTests.assert_MISSING(\"monster|alkilith|mtf\");\n            commonTests.assert_MISSING(\"monster|animated object (huge)|phb\");\n            commonTests.assert_MISSING(\"monster|ape|mm\");\n            commonTests.assert_Present(\"monster|ape|xmm\");\n            commonTests.assert_MISSING(\"monster|ash zombie|lmop\");\n            commonTests.assert_Present(\"monster|ash zombie|pabtso\");\n            commonTests.assert_MISSING(\"monster|awakened shrub|mm\");\n            commonTests.assert_Present(\"monster|awakened shrub|xmm\");\n            commonTests.assert_MISSING(\"monster|beast of the land|tce\");\n            commonTests.assert_Present(\"monster|beast of the land|xphb\");\n            commonTests.assert_MISSING(\"monster|bestial spirit (air)|tce\");\n            commonTests.assert_Present(\"monster|bestial spirit (air)|xphb\");\n            commonTests.assert_MISSING(\"monster|cat|mm\");\n            commonTests.assert_Present(\"monster|cat|xmm\");\n            commonTests.assert_Present(\"monster|derro savant|mpmm\");\n            commonTests.assert_MISSING(\"monster|derro savant|mtf\");\n            commonTests.assert_MISSING(\"monster|derro savant|oota\");\n            commonTests.assert_Present(\"monster|sibriex|mpmm\");\n            commonTests.assert_MISSING(\"monster|sibriex|mtf\");\n\n            commonTests.assert_MISSING(\"object|trebuchet|dmg\");\n            commonTests.assert_Present(\"object|trebuchet|xdmg\");\n\n            commonTests.assert_MISSING(\"optfeature|ambush|tce\");\n            commonTests.assert_Present(\"optfeature|ambush|xphb\");\n            commonTests.assert_MISSING(\"optfeature|investment of the chain master|tce\");\n            commonTests.assert_Present(\"optfeature|investment of the chain master|xphb\");\n\n            commonTests.assert_MISSING(\"reward|blessing of weapon enhancement|dmg\");\n            commonTests.assert_Present(\"reward|blessing of weapon enhancement|xdmg\");\n            commonTests.assert_MISSING(\"reward|blessing of wound closure|dmg\");\n            commonTests.assert_Present(\"reward|blessing of wound closure|xdmg\");\n            commonTests.assert_MISSING(\"reward|boon of combat prowess|dmg\");\n            commonTests.assert_MISSING(\"reward|boon of dimensional travel|dmg\");\n            commonTests.assert_MISSING(\"reward|boon of fate|dmg\");\n            commonTests.assert_MISSING(\"reward|boon of fortitude|dmg\");\n            commonTests.assert_Present(\"reward|boon of high magic|dmg\");\n\n            commonTests.assert_MISSING(\"sense|blindsight|phb\");\n            commonTests.assert_Present(\"sense|blindsight|xphb\");\n\n            commonTests.assert_MISSING(\"skill|athletics|phb\");\n            commonTests.assert_Present(\"skill|athletics|xphb\");\n\n            commonTests.assert_MISSING(\"spell|acid splash|phb\");\n            commonTests.assert_Present(\"spell|acid splash|xphb\");\n            commonTests.assert_Present(\"spell|aganazzar's scorcher|xge\");\n            commonTests.assert_MISSING(\"spell|blade barrier|phb\");\n            commonTests.assert_Present(\"spell|blade barrier|xphb\");\n            commonTests.assert_MISSING(\"spell|feeblemind|phb\");\n            commonTests.assert_Present(\"spell|illusory dragon|xge\");\n            commonTests.assert_MISSING(\"spell|illusory script|phb\");\n            commonTests.assert_Present(\"spell|illusory script|xphb\");\n            commonTests.assert_Present(\"spell|wrath of nature|xge\");\n\n            commonTests.assert_MISSING(\"status|surprised|phb\");\n            commonTests.assert_Present(\"status|surprised|xphb\");\n\n            commonTests.assert_MISSING(\"trap|collapsing roof|dmg\");\n            commonTests.assert_Present(\"trap|collapsing roof|xdmg\");\n            commonTests.assert_MISSING(\"trap|falling net|dmg\");\n            commonTests.assert_Present(\"trap|falling net|xdmg\");\n            commonTests.assert_MISSING(\"trap|pits|dmg\");\n            commonTests.assert_MISSING(\"trap|poison darts|dmg\");\n            commonTests.assert_Present(\"trap|poison needle trap|xge\");\n            commonTests.assert_MISSING(\"trap|poison needle|dmg\");\n            commonTests.assert_Present(\"trap|poisoned darts|xdmg\");\n            commonTests.assert_MISSING(\"trap|rolling sphere|dmg\");\n            commonTests.assert_Present(\"trap|rolling stone|xdmg\");\n\n            commonTests.assert_MISSING(\"vehicle|apparatus of kwalish|dmg\");\n            commonTests.assert_Present(\"vehicle|apparatus of kwalish|xdmg\");\n\n            // Classes, subclasses, class features, and subclass features\n\n            commonTests.assert_Present(\"classtype|artificer|efa\");\n            commonTests.assert_MISSING(\"classtype|artificer|tce\");\n\n            // \"Path of Wild Magic|Barbarian||Wild Magic|TCE|3\",\n            // \"Bolstering Magic|Barbarian||Wild Magic|TCE|6\",\n            // \"Unstable Backlash|Barbarian||Wild Magic|TCE|10\",\n            // \"Controlled Surge|Barbarian||Wild Magic|TCE|14\",\n\n            commonTests.assert_MISSING(\"classtype|barbarian|phb\");\n            commonTests.assert_Present(\"classtype|barbarian|xphb\");\n\n            commonTests.assert_MISSING(\"subclass|path of wild magic|barbarian|phb|tce\");\n            commonTests.assert_Present(\"subclass|path of wild magic|barbarian|xphb|tce\");\n\n            commonTests.assert_Present(\"subclassfeature|bolstering magic|barbarian|phb|wild magic|tce|6|tce\");\n            commonTests.assert_Present(\"subclassfeature|controlled surge|barbarian|phb|wild magic|tce|14|tce\");\n            commonTests.assert_Present(\"subclassfeature|magic awareness|barbarian|phb|wild magic|tce|3|tce\");\n            commonTests.assert_Present(\"subclassfeature|path of wild magic|barbarian|phb|wild magic|tce|3|tce\");\n            commonTests.assert_Present(\"subclassfeature|unstable backlash|barbarian|phb|wild magic|tce|10|tce\");\n            commonTests.assert_Present(\"subclassfeature|wild surge|barbarian|phb|wild magic|tce|3|tce\");\n\n            // \"Thief|Rogue||Thief||3\",\n            // \"Supreme Sneak|Rogue||Thief||9\",\n            // \"Use Magic Device|Rogue||Thief||13\",\n            // \"Thief's Reflexes|Rogue||Thief||17\"\n\n            commonTests.assert_MISSING(\"classtype|rogue|phb\");\n            commonTests.assert_Present(\"classtype|rogue|xphb\");\n\n            commonTests.assert_MISSING(\"subclass|thief|rogue|phb|phb\");\n            commonTests.assert_MISSING(\"subclass|thief|rogue|xphb|phb\");\n            commonTests.assert_Present(\"subclass|thief|rogue|xphb|xphb\");\n\n            commonTests.assert_MISSING(\"subclassfeature|thief|rogue|phb|thief|phb|3|phb\");\n            commonTests.assert_Present(\"subclassfeature|thief|rogue|xphb|thief|xphb|3|xphb\");\n            commonTests.assert_MISSING(\"subclassfeature|supreme sneak|rogue|phb|thief|phb|9|phb\");\n            commonTests.assert_Present(\"subclassfeature|supreme sneak|rogue|xphb|thief|xphb|9|xphb\");\n            commonTests.assert_MISSING(\"subclassfeature|use magic device|rogue|phb|thief|phb|13|phb\");\n            commonTests.assert_Present(\"subclassfeature|use magic device|rogue|xphb|thief|xphb|13|xphb\");\n            commonTests.assert_MISSING(\"subclassfeature|thief's reflexes|rogue|phb|thief|phb|17|phb\");\n            commonTests.assert_Present(\"subclassfeature|thief's reflexes|rogue|xphb|thief|xphb|17|xphb\");\n\n            // Races and subraces\n\n            commonTests.assert_MISSING(\"race|bugbear|erlw\");\n            commonTests.assert_Present(\"race|bugbear|mpmm\");\n            commonTests.assert_MISSING(\"race|bugbear|vgm\");\n            commonTests.assert_MISSING(\"race|human|phb\");\n            commonTests.assert_Present(\"race|human|xphb\");\n            commonTests.assert_MISSING(\"race|tiefling|phb\");\n            commonTests.assert_Present(\"race|tiefling|xphb\");\n            commonTests.assert_Present(\"race|warforged|efa\");\n            commonTests.assert_MISSING(\"race|warforged|erlw\");\n            commonTests.assert_MISSING(\"race|yuan-ti pureblood|vgm\");\n            commonTests.assert_Present(\"race|yuan-ti|mpmm\");\n\n            commonTests.assert_MISSING(\"subrace|genasi (air)|genasi|eepc|eepc\");\n            commonTests.assert_Present(\"subrace|genasi (air)|genasi|mpmm|mpmm\");\n            commonTests.assert_MISSING(\"subrace|human|human|phb|phb\");\n            commonTests.assert_Present(\"subrace|tiefling (zariel)|tiefling|phb|mtf\");\n            commonTests.assert_MISSING(\"subrace|tiefling|tiefling|phb|phb\");\n            commonTests.assert_Present(\"subrace|vampire (ixalan)|vampire|psz|psx\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/java/dev/ebullient/convert/tools/dnd5e/FilterAllTest.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.io.IOException;\nimport java.nio.file.Path;\n\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\nimport com.fasterxml.jackson.databind.JsonNode;\n\nimport dev.ebullient.convert.TestUtils;\nimport dev.ebullient.convert.tools.dnd5e.CommonDataTests.TestInput;\nimport dev.ebullient.convert.tools.dnd5e.Json2QuteMonster.MonsterFields;\nimport io.quarkus.test.junit.QuarkusTest;\n\n@QuarkusTest\npublic class FilterAllTest {\n\n    static CommonDataTests commonTests;\n    static final TestInput testInput = TestInput.all;\n    static final Path outputPath = TestUtils.OUTPUT_5E_DATA.resolve(testInput.name());\n\n    @BeforeAll\n    public static void setupDir() throws Exception {\n        outputPath.toFile().mkdirs();\n        // extra escaping for regex as we're reading from string\n        String config = \"\"\"\n                {\n                    \"reprintBehavior\": \"all\",\n                    \"sources\": {\n                        \"book\": [\n                            \"XGE\",\n                            \"XMM\",\n                            \"FRAiF\"\n                        ],\n                        \"adventure\": [\n                            \"OotA\",\n                            \"DIP\"\n                        ],\n                        \"reference\": [\n                            \"*\"\n                        ]\n                    },\n                    \"include\": [\n                        \"race|changeling|mpmm\"\n                    ],\n                    \"exclude\": [\n                        \"monster|expert|dc\",\n                        \"monster|expert|sdw\",\n                        \"monster|expert|slw\"\n                    ],\n                    \"excludePattern\": [\n                        \"race\\\\\\\\|.*\\\\\\\\|dmg\"\n                    ],\n                    \"paths\": {\n                        \"rules\": \"rules/\",\n                        \"compendium\": \"\"\n                    },\n                    \"images\": {\n                        \"copyInternal\": false\n                    },\n                    \"useDiceRoller\" : true\n                }\n                \"\"\".stripIndent();\n        commonTests = new CommonDataTests(testInput, config, TestUtils.PATH_5E_TOOLS_DATA);\n    }\n\n    @AfterAll\n    public static void done() throws IOException {\n        commonTests.afterAll(outputPath);\n    }\n\n    @AfterEach\n    public void cleanup() throws Exception {\n        commonTests.afterEach();\n    }\n\n    @Test\n    public void testKeyIndex() throws Exception {\n        commonTests.testKeyIndex(outputPath);\n\n        // All sources, but reprints will be followed.\n        // PHB elements should be missing/replaced by XPHB equivalents (e.g.)\n        if (commonTests.dataPresent) {\n            var config = commonTests.config;\n\n            assertThat(config.sourceIncluded(\"srd\")).isTrue();\n            assertThat(config.sourceIncluded(\"basicrules\")).isTrue();\n            assertThat(config.sourceIncluded(\"srd52\")).isTrue();\n            assertThat(config.sourceIncluded(\"basicRules2024\")).isTrue();\n\n            assertThat(config.sourceIncluded(\"DMG\")).isTrue();\n            assertThat(config.sourceIncluded(\"PHB\")).isTrue();\n\n            assertThat(config.sourceIncluded(\"XDMG\")).isTrue();\n            assertThat(config.sourceIncluded(\"XPHB\")).isTrue();\n\n            commonTests.assert_Present(\"action|attack|phb\");\n            commonTests.assert_Present(\"action|attack|xphb\");\n            commonTests.assert_Present(\"action|cast a spell|phb\");\n            commonTests.assert_Present(\"action|disengage|phb\");\n            commonTests.assert_Present(\"action|disengage|xphb\");\n\n            commonTests.assert_Present(\"feat|alert|phb\");\n            commonTests.assert_Present(\"feat|alert|xphb\");\n            commonTests.assert_Present(\"feat|dueling|xphb\");\n            commonTests.assert_Present(\"feat|grappler|phb\");\n            commonTests.assert_Present(\"feat|grappler|xphb\");\n            commonTests.assert_Present(\"feat|mobile|phb\");\n            commonTests.assert_Present(\"feat|moderately armored|phb\");\n            commonTests.assert_Present(\"feat|moderately armored|xphb\");\n\n            commonTests.assert_Present(\"variantrule|facing|dmg\");\n            commonTests.assert_Present(\"variantrule|falling|xge\");\n            commonTests.assert_Present(\"variantrule|familiars|mm\");\n            commonTests.assert_Present(\"variantrule|simultaneous effects|xge\");\n            commonTests.assert_Present(\"variantrule|simultaneous effects|xphb\");\n\n            commonTests.assert_Present(\"background|sage|phb\");\n            commonTests.assert_Present(\"background|sage|xphb\");\n            commonTests.assert_Present(\"background|baldur's gate acolyte|bgdia\");\n\n            commonTests.assert_Present(\"condition|blinded|phb\");\n            commonTests.assert_Present(\"condition|blinded|xphb\");\n\n            commonTests.assert_Present(\"deity|auril|faerûnian|frhof\");\n            commonTests.assert_Present(\"deity|auril|faerûnian|scag\");\n            commonTests.assert_Present(\"deity|auril|forgotten realms|phb\");\n            commonTests.assert_Present(\"deity|chemosh|dragonlance|dsotdq\");\n            commonTests.assert_Present(\"deity|chemosh|dragonlance|phb\");\n            commonTests.assert_Present(\"deity|ehlonna|greyhawk|phb\");\n            commonTests.assert_Present(\"deity|ehlonna|greyhawk|xdmg\");\n            commonTests.assert_Present(\"deity|gruumsh|dawn war|dmg\");\n            commonTests.assert_Present(\"deity|gruumsh|exandria|egw\");\n            commonTests.assert_Present(\"deity|gruumsh|nonhuman|phb\");\n            commonTests.assert_Present(\"deity|gruumsh|orc|scag\");\n            commonTests.assert_Present(\"deity|gruumsh|orc|vgm\");\n            commonTests.assert_Present(\"deity|the traveler|eberron|erlw\");\n            commonTests.assert_Present(\"deity|the traveler|eberron|phb\");\n            commonTests.assert_Present(\"deity|the traveler|exandria|egw\");\n\n            commonTests.assert_Present(\"disease|cackle fever|dmg\");\n            commonTests.assert_Present(\"disease|cackle fever|xdmg\");\n\n            commonTests.assert_Present(\"hazard|quicksand pit|xdmg\");\n            commonTests.assert_Present(\"hazard|quicksand|dmg\");\n            commonTests.assert_Present(\"hazard|razorvine|dmg\");\n            commonTests.assert_Present(\"hazard|razorvine|xdmg\");\n\n            commonTests.assert_Present(\"itemgroup|arcane focus|phb\");\n            commonTests.assert_Present(\"itemgroup|arcane focus|xphb\");\n            commonTests.assert_Present(\"itemgroup|carpet of flying|dmg\");\n            commonTests.assert_Present(\"itemgroup|carpet of flying|xdmg\");\n            commonTests.assert_Present(\"itemgroup|ioun stone|dmg\");\n            commonTests.assert_Present(\"itemgroup|ioun stone|llk\");\n            commonTests.assert_Present(\"itemgroup|ioun stone|xdmg\");\n            commonTests.assert_Present(\"itemgroup|musical instrument|phb\");\n            commonTests.assert_Present(\"itemgroup|musical instrument|scag\");\n            commonTests.assert_Present(\"itemgroup|musical instrument|xphb\");\n            commonTests.assert_Present(\"itemgroup|spell scroll|dmg\");\n            commonTests.assert_Present(\"itemgroup|spell scroll|xdmg\");\n\n            commonTests.assert_Present(\"itemproperty|2h|phb\");\n            commonTests.assert_Present(\"itemproperty|2h|xphb\");\n            commonTests.assert_Present(\"itemproperty|bf|dmg\");\n            commonTests.assert_Present(\"itemproperty|bf|xdmg\");\n            commonTests.assert_Present(\"itemproperty|s|phb\");\n\n            commonTests.assert_Present(\"itemtype|$c|phb\");\n            commonTests.assert_Present(\"itemtype|$c|xphb\");\n            commonTests.assert_Present(\"itemtype|$g|dmg\");\n            commonTests.assert_Present(\"itemtype|$g|xdmg\");\n            commonTests.assert_Present(\"itemtype|sc|dmg\");\n            commonTests.assert_Present(\"itemtype|sc|xphb\");\n            commonTests.assert_Present(\"itemtypeadditionalentries|gs|phb|xge\");\n\n            commonTests.assert_Present(\"item|+1 rod of the pact keeper|dmg\");\n            commonTests.assert_Present(\"item|+1 rod of the pact keeper|xdmg\");\n            commonTests.assert_Present(\"item|+2 wraps of unarmed power|xdmg\");\n            commonTests.assert_Present(\"item|+2 wraps of unarmed prowess|bmt\");\n            commonTests.assert_Present(\"item|acid (vial)|phb\");\n            commonTests.assert_Present(\"item|acid absorbing tattoo|tce\");\n            commonTests.assert_Present(\"item|acid|xphb\");\n            commonTests.assert_Present(\"item|alchemist's doom|scc\");\n            commonTests.assert_Present(\"item|alchemist's fire (flask)|phb\");\n            commonTests.assert_Present(\"item|alchemist's fire|xphb\");\n            commonTests.assert_Present(\"item|alchemist's supplies|phb\");\n            commonTests.assert_Present(\"item|alchemist's supplies|xphb\");\n            commonTests.assert_Present(\"item|amulet of health|dmg\");\n            commonTests.assert_Present(\"item|amulet of health|xdmg\");\n            commonTests.assert_Present(\"item|amulet of proof against detection and location|dmg\");\n            commonTests.assert_Present(\"item|amulet of proof against detection and location|xdmg\");\n            commonTests.assert_Present(\"item|armor of invulnerability|dmg\");\n            commonTests.assert_Present(\"item|armor of invulnerability|xdmg\");\n            commonTests.assert_Present(\"item|automatic pistol|dmg\");\n            commonTests.assert_Present(\"item|automatic rifle|dmg\");\n            commonTests.assert_Present(\"item|automatic rifle|xdmg\");\n            commonTests.assert_Present(\"item|ball bearings (bag of 1,000)|phb\");\n            commonTests.assert_Present(\"item|ball bearings|xphb\");\n            commonTests.assert_Present(\"item|ball bearing|phb\");\n            commonTests.assert_Present(\"item|chain (10 feet)|phb\");\n            commonTests.assert_Present(\"item|chain|xphb\");\n\n            commonTests.assert_Present(\"monster|abjurer wizard|mpmm\");\n            commonTests.assert_Present(\"monster|abjurer|vgm\");\n            commonTests.assert_Present(\"monster|alkilith|mpmm\");\n            commonTests.assert_Present(\"monster|alkilith|mtf\");\n            commonTests.assert_Present(\"monster|animated object (huge)|phb\");\n            commonTests.assert_Present(\"monster|ape|mm\");\n            commonTests.assert_Present(\"monster|ape|xmm\");\n            commonTests.assert_Present(\"monster|ash zombie|lmop\");\n            commonTests.assert_Present(\"monster|ash zombie|pabtso\");\n            commonTests.assert_Present(\"monster|awakened shrub|mm\");\n            commonTests.assert_Present(\"monster|awakened shrub|xmm\");\n            commonTests.assert_Present(\"monster|beast of the land|tce\");\n            commonTests.assert_Present(\"monster|beast of the land|xphb\");\n            commonTests.assert_Present(\"monster|bestial spirit (air)|tce\");\n            commonTests.assert_Present(\"monster|bestial spirit (air)|xphb\");\n            commonTests.assert_Present(\"monster|cat|mm\");\n            commonTests.assert_Present(\"monster|cat|xmm\");\n            commonTests.assert_Present(\"monster|derro savant|mpmm\");\n            commonTests.assert_Present(\"monster|derro savant|mtf\");\n            commonTests.assert_Present(\"monster|derro savant|oota\");\n            commonTests.assert_Present(\"monster|sibriex|mpmm\");\n            commonTests.assert_Present(\"monster|sibriex|mtf\");\n\n            commonTests.assert_Present(\"object|trebuchet|dmg\");\n            commonTests.assert_Present(\"object|trebuchet|xdmg\");\n\n            commonTests.assert_Present(\"optfeature|ambush|tce\");\n            commonTests.assert_Present(\"optfeature|ambush|xphb\");\n            commonTests.assert_Present(\"optfeature|investment of the chain master|tce\");\n            commonTests.assert_Present(\"optfeature|investment of the chain master|xphb\");\n\n            commonTests.assert_Present(\"reward|blessing of weapon enhancement|dmg\");\n            commonTests.assert_Present(\"reward|blessing of weapon enhancement|xdmg\");\n            commonTests.assert_Present(\"reward|blessing of wound closure|dmg\");\n            commonTests.assert_Present(\"reward|blessing of wound closure|xdmg\");\n            commonTests.assert_Present(\"reward|boon of combat prowess|dmg\");\n            commonTests.assert_Present(\"reward|boon of dimensional travel|dmg\");\n            commonTests.assert_Present(\"reward|boon of fate|dmg\");\n            commonTests.assert_Present(\"reward|boon of fortitude|dmg\");\n            commonTests.assert_Present(\"reward|boon of high magic|dmg\");\n\n            commonTests.assert_Present(\"sense|blindsight|phb\");\n            commonTests.assert_Present(\"sense|blindsight|xphb\");\n\n            commonTests.assert_Present(\"skill|athletics|phb\");\n            commonTests.assert_Present(\"skill|athletics|xphb\");\n\n            commonTests.assert_Present(\"spell|acid splash|phb\");\n            commonTests.assert_Present(\"spell|acid splash|xphb\");\n            commonTests.assert_Present(\"spell|aganazzar's scorcher|xge\");\n            commonTests.assert_Present(\"spell|blade barrier|phb\");\n            commonTests.assert_Present(\"spell|blade barrier|xphb\");\n            commonTests.assert_Present(\"spell|feeblemind|phb\");\n            commonTests.assert_Present(\"spell|illusory dragon|xge\");\n            commonTests.assert_Present(\"spell|illusory script|phb\");\n            commonTests.assert_Present(\"spell|illusory script|xphb\");\n            commonTests.assert_Present(\"spell|wrath of nature|xge\");\n\n            commonTests.assert_Present(\"status|surprised|phb\");\n            commonTests.assert_Present(\"status|surprised|xphb\");\n\n            commonTests.assert_Present(\"subclass|thief|rogue|phb|phb\");\n            commonTests.assert_MISSING(\"subclass|thief|rogue|xphb|phb\");\n            commonTests.assert_Present(\"subclass|thief|rogue|xphb|xphb\");\n\n            commonTests.assert_Present(\"trap|collapsing roof|dmg\");\n            commonTests.assert_Present(\"trap|collapsing roof|xdmg\");\n            commonTests.assert_Present(\"trap|falling net|dmg\");\n            commonTests.assert_Present(\"trap|falling net|xdmg\");\n            commonTests.assert_Present(\"trap|pits|dmg\");\n            commonTests.assert_Present(\"trap|hidden pit|xdmg\");\n            commonTests.assert_Present(\"trap|poison darts|dmg\");\n            commonTests.assert_Present(\"trap|poison needle trap|xge\");\n            commonTests.assert_Present(\"trap|poison needle|dmg\");\n            commonTests.assert_Present(\"trap|poisoned darts|xdmg\");\n            commonTests.assert_Present(\"trap|rolling sphere|dmg\");\n            commonTests.assert_Present(\"trap|rolling stone|xdmg\");\n\n            commonTests.assert_Present(\"vehicle|apparatus of kwalish|dmg\");\n            commonTests.assert_Present(\"vehicle|apparatus of kwalish|xdmg\");\n\n            // Classes, subclasses, class features, and subclass features\n\n            commonTests.assert_Present(\"classtype|artificer|efa\");\n            commonTests.assert_Present(\"classtype|artificer|tce\");\n\n            // \"Path of Wild Magic|Barbarian||Wild Magic|TCE|3\",\n            // \"Bolstering Magic|Barbarian||Wild Magic|TCE|6\",\n            // \"Unstable Backlash|Barbarian||Wild Magic|TCE|10\",\n            // \"Controlled Surge|Barbarian||Wild Magic|TCE|14\",\n\n            commonTests.assert_Present(\"classtype|barbarian|phb\");\n            commonTests.assert_Present(\"classtype|barbarian|xphb\");\n\n            commonTests.assert_Present(\"subclass|path of wild magic|barbarian|phb|tce\");\n            commonTests.assert_MISSING(\"subclass|path of wild magic|barbarian|xphb|tce\");\n\n            commonTests.assert_Present(\"subclassfeature|bolstering magic|barbarian|phb|wild magic|tce|6|tce\");\n            commonTests.assert_Present(\"subclassfeature|controlled surge|barbarian|phb|wild magic|tce|14|tce\");\n            commonTests.assert_Present(\"subclassfeature|magic awareness|barbarian|phb|wild magic|tce|3|tce\");\n            commonTests.assert_Present(\"subclassfeature|path of wild magic|barbarian|phb|wild magic|tce|3|tce\");\n            commonTests.assert_Present(\"subclassfeature|unstable backlash|barbarian|phb|wild magic|tce|10|tce\");\n            commonTests.assert_Present(\"subclassfeature|wild surge|barbarian|phb|wild magic|tce|3|tce\");\n\n            // \"Thief|Rogue||Thief||3\",\n            // \"Supreme Sneak|Rogue||Thief||9\",\n            // \"Use Magic Device|Rogue||Thief||13\",\n            // \"Thief's Reflexes|Rogue||Thief||17\"\n\n            commonTests.assert_Present(\"classtype|rogue|phb\");\n            commonTests.assert_Present(\"classtype|rogue|xphb\");\n\n            commonTests.assert_Present(\"subclass|thief|rogue|phb|phb\");\n            commonTests.assert_MISSING(\"subclass|thief|rogue|xphb|phb\");\n            commonTests.assert_Present(\"subclass|thief|rogue|xphb|xphb\");\n\n            commonTests.assert_Present(\"subclassfeature|thief|rogue|phb|thief|phb|3|phb\");\n            commonTests.assert_Present(\"subclassfeature|thief|rogue|xphb|thief|xphb|3|xphb\");\n            commonTests.assert_Present(\"subclassfeature|supreme sneak|rogue|phb|thief|phb|9|phb\");\n            commonTests.assert_Present(\"subclassfeature|supreme sneak|rogue|xphb|thief|xphb|9|xphb\");\n            commonTests.assert_Present(\"subclassfeature|use magic device|rogue|phb|thief|phb|13|phb\");\n            commonTests.assert_Present(\"subclassfeature|use magic device|rogue|xphb|thief|xphb|13|xphb\");\n            commonTests.assert_Present(\"subclassfeature|thief's reflexes|rogue|phb|thief|phb|17|phb\");\n            commonTests.assert_Present(\"subclassfeature|thief's reflexes|rogue|xphb|thief|xphb|17|xphb\");\n\n            // Races and subraces\n\n            commonTests.assert_Present(\"race|bugbear|erlw\");\n            commonTests.assert_Present(\"race|bugbear|mpmm\");\n            commonTests.assert_Present(\"race|bugbear|vgm\");\n            commonTests.assert_Present(\"race|human|phb\");\n            commonTests.assert_Present(\"race|human|xphb\");\n            commonTests.assert_Present(\"race|tiefling|phb\");\n            commonTests.assert_Present(\"race|tiefling|xphb\");\n            commonTests.assert_Present(\"race|warforged|efa\");\n            commonTests.assert_Present(\"race|warforged|erlw\");\n            commonTests.assert_Present(\"race|yuan-ti pureblood|vgm\");\n            commonTests.assert_Present(\"race|yuan-ti|mpmm\");\n\n            commonTests.assert_Present(\"subrace|genasi (air)|genasi|eepc|eepc\");\n            commonTests.assert_Present(\"subrace|genasi (air)|genasi|mpmm|mpmm\");\n            commonTests.assert_Present(\"subrace|human|human|phb|phb\");\n            commonTests.assert_Present(\"subrace|tiefling (zariel)|tiefling|phb|mtf\");\n            commonTests.assert_Present(\"subrace|tiefling|tiefling|phb|phb\");\n            commonTests.assert_Present(\"subrace|vampire (ixalan)|vampire|psz|psx\");\n        }\n    }\n\n    @Test\n    public void testAdventures() {\n        commonTests.testAdventures(outputPath);\n    }\n\n    @Test\n    public void testBackgroundList() {\n        commonTests.testBackgroundList(outputPath);\n    }\n\n    @Test\n    public void testBooks() {\n        commonTests.testBookList(outputPath);\n    }\n\n    @Test\n    public void testClassList() {\n        commonTests.testClassList(outputPath);\n    }\n\n    @Test\n    public void testDeckList() {\n        commonTests.testDeckList(outputPath);\n    }\n\n    @Test\n    public void testDeityList() {\n        commonTests.testDeityList(outputPath);\n    }\n\n    @Test\n    public void testFacilityList() {\n        commonTests.testFacilityList(outputPath);\n    }\n\n    @Test\n    public void testFeatList() {\n        commonTests.testFeatList(outputPath);\n    }\n\n    @Test\n    public void testItemList() {\n        commonTests.testItemList(outputPath);\n    }\n\n    @Test\n    public void testMagicVariants() {\n        if (!TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) {\n            return;\n        }\n\n        // \"requires\":[{\"type\":\"HA\"},{\"type\":\"MA\"}], \"excludes\": {\"name\": \"Hide Armor\" }\n        JsonNode adamantineArmor = commonTests.index.getOrigin(\"magicvariant|adamantine armor|dmg\");\n        assertThat(adamantineArmor).isNotNull();\n\n        // \"requires\":[{\"type\":\"M\"}],\"excludes\":{\"property\":\"2H\"}\n        JsonNode armBlade = commonTests.index.getOrigin(\"magicvariant|armblade|erlw\");\n        assertThat(armBlade).isNotNull();\n\n        // \"requires\":[{\"type\":\"R\"},{\"type\":\"T\"}],\n        JsonNode arrowSlaying = commonTests.index.getOrigin(\"magicvariant|arrow of slaying (*)|dmg\");\n        assertThat(arrowSlaying).isNotNull();\n\n        // \"requires\":[{\"sword\":true}]\n        JsonNode luckBlade = commonTests.index.getOrigin(\"magicvariant|luck blade|dmg\");\n        assertThat(luckBlade).isNotNull();\n\n        // \"requires\":[{\"type\":\"SCF\",\"scfType\":\"arcane\"}],\n        // \"excludes\":{\"name\":[\"Staff\",\"Rod\",\"Wand\"]}\n        JsonNode orbOfShielding = commonTests.index.getOrigin(\"magicvariant|orb of shielding (irian quartz)|erlw\");\n        assertThat(orbOfShielding).isNotNull();\n\n        JsonNode x;\n\n        x = commonTests.index.getOrigin(\"item|arrow|phb\");\n        assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, arrowSlaying))\n                .describedAs(\"arrowSlaying: Arrow has one required property\")\n                .isTrue();\n\n        x = commonTests.index.getOrigin(\"item|crystal|phb\");\n        assertThat(MagicVariant.INSTANCE.hasExcludedProperty(x, armBlade))\n                .describedAs(\"armBlade: Crystal is not a two-handed weapon (2H)\")\n                .isFalse();\n        assertThat(MagicVariant.INSTANCE.hasExcludedProperty(x, orbOfShielding))\n                .describedAs(\"orbOfShielding: Crystal does not have excluded name\")\n                .isFalse();\n        assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, armBlade))\n                .describedAs(\"armBlade: Crystal is not a melee type (M)\")\n                .isFalse();\n        assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, arrowSlaying))\n                .describedAs(\"arrowSlaying: Crystal does not have either required property\")\n                .isFalse();\n        assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, luckBlade))\n                .describedAs(\"luckBlade: Crystal is not a sword\")\n                .isFalse();\n        assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, orbOfShielding))\n                .describedAs(\"orbOfShielding: Crystal has required property (SCF)\")\n                .isTrue();\n\n        x = commonTests.index.getOrigin(\"item|dagger|phb\");\n        assertThat(MagicVariant.INSTANCE.hasExcludedProperty(x, armBlade))\n                .describedAs(\"armBlade: Dagger is not a two-handed weapon (2H)\")\n                .isFalse();\n        assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, armBlade))\n                .describedAs(\"armBlade: Dagger is a melee type (M)\")\n                .isTrue();\n        assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, luckBlade))\n                .describedAs(\"luckBlade: Dagger is not a sword\")\n                .isFalse();\n        assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, orbOfShielding))\n                .describedAs(\"orbOfShielding: Dagger does not have the required property (SCF / arcane)\")\n                .isFalse();\n\n        x = commonTests.index.getOrigin(\"item|greatsword|phb\");\n        assertThat(MagicVariant.INSTANCE.hasExcludedProperty(x, armBlade))\n                .describedAs(\"armBlade: Greatsword is a two-handed weapon (2H)\")\n                .isTrue();\n        assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, luckBlade))\n                .describedAs(\"luckBlade: Greatsword is a sword\")\n                .isTrue();\n\n        x = commonTests.index.getOrigin(\"item|scimitar|phb\");\n        assertThat(MagicVariant.INSTANCE.hasExcludedProperty(x, armBlade))\n                .describedAs(\"armBlade: Scimitar is not a two-handed weapon (2H)\")\n                .isFalse();\n        assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, armBlade))\n                .describedAs(\"armBlade: Scimitar is a melee type (M)\")\n                .isTrue();\n        assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, luckBlade))\n                .describedAs(\"luckBlade: Scimitar is a sword\")\n                .isTrue();\n\n        x = commonTests.index.getOrigin(\"item|wand|phb\");\n        assertThat(MagicVariant.INSTANCE.hasExcludedProperty(x, orbOfShielding))\n                .describedAs(\"orbOfShielding: Wand is an excluded name\")\n                .isTrue();\n        assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, orbOfShielding))\n                .describedAs(\"orbOfShielding: Wand has the required property (SCF / arcane)\")\n                .isTrue();\n\n        x = commonTests.index.getOrigin(\"item|wooden staff|phb\");\n        assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, orbOfShielding))\n                .describedAs(\"orbOfShielding: Wooden staff (SCF / druid) does not have all required properties (SCF / arcane)\")\n                .isFalse();\n\n        x = commonTests.index.getOrigin(\"item|chain mail|phb\");\n        assertThat(MagicVariant.INSTANCE.hasExcludedProperty(x, adamantineArmor))\n                .describedAs(\"adamantineArmor: Chain Mail is not excluded\")\n                .isFalse();\n        assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, adamantineArmor))\n                .describedAs(\"adamantineArmor: Chain Mail is HA\")\n                .isTrue();\n\n        x = commonTests.index.getOrigin(\"item|hide armor|phb\");\n        assertThat(MagicVariant.INSTANCE.hasExcludedProperty(x, adamantineArmor))\n                .describedAs(\"adamantineArmor: Hide Armor is excluded\")\n                .isTrue();\n        assertThat(MagicVariant.INSTANCE.hasRequiredProperty(x, adamantineArmor))\n                .describedAs(\"adamantineArmor: Hide Armor is MA\")\n                .isTrue();\n    }\n\n    @Test\n    public void testMonsterList() {\n        commonTests.testMonsterList(outputPath);\n\n        if (!TestUtils.PATH_5E_TOOLS_DATA.toFile().exists()) {\n            return;\n        }\n\n        JsonNode x;\n\n        x = commonTests.index.getOrigin(\"monster|reduced-threat aboleth|tftyp\");\n        JsonNode hp = MonsterFields.hp.getFrom(x);\n        assertThat(hp).isNotNull();\n        assertThat(MonsterFields.average.getFrom(hp).toString())\n                .describedAs(\"Reduced Threat monsters should have a template with stat modifications applied\")\n                .isEqualTo(\"67.0\");\n        assertThat(MonsterFields.trait.getFrom(x).toPrettyString())\n                .describedAs(\"Reduced Threat monsters should have a template with stat modifications applied\")\n                .contains(\"Reduced Threat\");\n    }\n\n    @Test\n    public void testMonsterAlternateScores() {\n        commonTests.testMonsterAlternateScores(outputPath);\n    }\n\n    @Test\n    public void testMonsterYamlHeader() {\n        commonTests.testMonsterYamlHeader(outputPath);\n    }\n\n    @Test\n    public void testMonsterYamlBody() {\n        commonTests.testMonsterYamlBody(outputPath);\n    }\n\n    @Test\n    public void testMonster2024() {\n        commonTests.testMonster2024(outputPath);\n    }\n\n    @Test\n    public void testObjectList() {\n        commonTests.testObjectList(outputPath);\n    }\n\n    @Test\n    public void testOptionalFeatureList() {\n        commonTests.testOptionalFeatureList(outputPath);\n    }\n\n    @Test\n    public void testPsionicList() {\n        commonTests.testPsionicList(outputPath);\n    }\n\n    @Test\n    public void testRaceList() {\n        commonTests.testRaceList(outputPath);\n    }\n\n    @Test\n    public void testRewardList() {\n        commonTests.testRewardList(outputPath);\n    }\n\n    @Test\n    public void testRules() {\n        commonTests.testRules(outputPath);\n    }\n\n    @Test\n    public void testSpellList() {\n        commonTests.testSpellList(outputPath);\n    }\n\n    @Test\n    public void testTrapsHazardsList() {\n        commonTests.testTrapsHazardsList(outputPath);\n    }\n\n    @Test\n    public void testVehicleList() {\n        commonTests.testVehicleList(outputPath);\n    }\n}\n"
  },
  {
    "path": "src/test/java/dev/ebullient/convert/tools/dnd5e/FilterNoneTest.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.io.IOException;\nimport java.nio.file.Path;\n\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\nimport dev.ebullient.convert.TestUtils;\nimport dev.ebullient.convert.tools.dnd5e.CommonDataTests.TestInput;\nimport io.quarkus.test.junit.QuarkusTest;\n\n@QuarkusTest\npublic class FilterNoneTest {\n\n    static CommonDataTests commonTests;\n    static final TestInput testInput = TestInput.none;\n    static final Path outputPath = TestUtils.OUTPUT_5E_DATA.resolve(testInput.name());\n\n    @BeforeAll\n    public static void setupDir() throws Exception {\n        outputPath.toFile().mkdirs();\n        String config = \"\"\"\n                {\n                    \"images\": {\n                        \"copyInternal\": false\n                    }\n                }\n                \"\"\".stripIndent();\n        commonTests = new CommonDataTests(testInput, config, TestUtils.PATH_5E_TOOLS_DATA);\n    }\n\n    @AfterAll\n    public static void done() throws IOException {\n        commonTests.afterAll(outputPath);\n    }\n\n    @AfterEach\n    public void cleanup() throws Exception {\n        commonTests.afterEach();\n    }\n\n    @Test\n    public void testKeyIndex() throws Exception {\n        commonTests.testKeyIndex(outputPath);\n\n        // NONE: _newest_ across 2024 freerules, 5.1 SRD, and 2014 basic rules\n        // (2014 basic rules / srd content unless replaced by 2024 free rules content)\n\n        if (commonTests.dataPresent) {\n            var config = commonTests.config;\n\n            assertThat(config.noSources()).isTrue();\n\n            assertThat(config.sourceIncluded(\"srd\")).isFalse();\n            assertThat(config.sourceIncluded(\"basicrules\")).isFalse();\n            assertThat(config.sourceIncluded(\"srd52\")).isFalse();\n            assertThat(config.sourceIncluded(\"basicRules2024\")).isFalse();\n\n            assertThat(config.sourceIncluded(\"DMG\")).isFalse();\n            assertThat(config.sourceIncluded(\"PHB\")).isFalse();\n\n            assertThat(config.sourceIncluded(\"XDMG\")).isFalse();\n            assertThat(config.sourceIncluded(\"XPHB\")).isFalse();\n\n            commonTests.assert_MISSING(\"action|attack|phb\");\n            commonTests.assert_Present(\"action|attack|xphb\");\n            commonTests.assert_MISSING(\"action|cast a spell|phb\");\n            commonTests.assert_MISSING(\"action|disengage|phb\");\n            commonTests.assert_Present(\"action|disengage|xphb\");\n\n            commonTests.assert_MISSING(\"feat|alert|phb\");\n            commonTests.assert_Present(\"feat|alert|xphb\");\n            commonTests.assert_MISSING(\"feat|dueling|xphb\");\n            commonTests.assert_MISSING(\"feat|grappler|phb\");\n            commonTests.assert_Present(\"feat|grappler|xphb\");\n            commonTests.assert_MISSING(\"feat|mobile|phb\");\n            commonTests.assert_MISSING(\"feat|moderately armored|phb\");\n            commonTests.assert_MISSING(\"feat|moderately armored|xphb\");\n\n            commonTests.assert_MISSING(\"variantrule|facing|dmg\");\n            commonTests.assert_MISSING(\"variantrule|falling|xge\");\n            commonTests.assert_MISSING(\"variantrule|familiars|mm\");\n            commonTests.assert_MISSING(\"variantrule|simultaneous effects|xge\");\n            commonTests.assert_Present(\"variantrule|simultaneous effects|xphb\");\n\n            commonTests.assert_MISSING(\"background|sage|phb\");\n            commonTests.assert_Present(\"background|sage|xphb\");\n            commonTests.assert_MISSING(\"background|baldur's gate acolyte|bgdia\");\n\n            commonTests.assert_MISSING(\"condition|blinded|phb\");\n            commonTests.assert_Present(\"condition|blinded|xphb\");\n            commonTests.assert_Present(\"condition|blinded|xphb\");\n\n            commonTests.assert_MISSING(\"deity|auril|faerûnian|frhof\");\n            commonTests.assert_MISSING(\"deity|auril|faerûnian|scag\");\n            commonTests.assert_MISSING(\"deity|auril|forgotten realms|phb\");\n            commonTests.assert_MISSING(\"deity|chemosh|dragonlance|dsotdq\");\n            commonTests.assert_MISSING(\"deity|chemosh|dragonlance|phb\");\n            commonTests.assert_MISSING(\"deity|ehlonna|greyhawk|phb\");\n            commonTests.assert_MISSING(\"deity|ehlonna|greyhawk|xdmg\");\n            commonTests.assert_MISSING(\"deity|gruumsh|dawn war|dmg\");\n            commonTests.assert_MISSING(\"deity|gruumsh|exandria|egw\");\n            commonTests.assert_MISSING(\"deity|gruumsh|nonhuman|phb\");\n            commonTests.assert_MISSING(\"deity|gruumsh|orc|scag\");\n            commonTests.assert_MISSING(\"deity|gruumsh|orc|vgm\");\n            commonTests.assert_MISSING(\"deity|the traveler|eberron|erlw\");\n            commonTests.assert_MISSING(\"deity|the traveler|eberron|phb\");\n            commonTests.assert_MISSING(\"deity|the traveler|exandria|egw\");\n\n            commonTests.assert_MISSING(\"disease|cackle fever|dmg\"); // part of basic rules\n            commonTests.assert_Present(\"disease|cackle fever|xdmg\");\n\n            commonTests.assert_MISSING(\"hazard|quicksand pit|xdmg\");\n            commonTests.assert_MISSING(\"hazard|quicksand|dmg\");\n            commonTests.assert_MISSING(\"hazard|razorvine|dmg\");\n            commonTests.assert_MISSING(\"hazard|razorvine|xdmg\");\n\n            commonTests.assert_MISSING(\"itemgroup|arcane focus|phb\");\n            commonTests.assert_MISSING(\"itemgroup|arcane focus|xphb\");\n            commonTests.assert_MISSING(\"itemgroup|carpet of flying|dmg\");\n            commonTests.assert_Present(\"itemgroup|carpet of flying|xdmg\");\n            commonTests.assert_MISSING(\"itemgroup|ioun stone|dmg\");\n            commonTests.assert_MISSING(\"itemgroup|ioun stone|llk\");\n            commonTests.assert_Present(\"itemgroup|ioun stone|xdmg\");\n            commonTests.assert_MISSING(\"itemgroup|musical instrument|phb\");\n            commonTests.assert_MISSING(\"itemgroup|musical instrument|scag\");\n            commonTests.assert_Present(\"itemgroup|musical instrument|xphb\");\n            commonTests.assert_MISSING(\"itemgroup|spell scroll|dmg\");\n            commonTests.assert_Present(\"itemgroup|spell scroll|xdmg\");\n\n            commonTests.assert_MISSING(\"itemproperty|2h|phb\");\n            commonTests.assert_Present(\"itemproperty|2h|xphb\");\n            commonTests.assert_MISSING(\"itemproperty|bf|dmg\");\n            commonTests.assert_MISSING(\"itemproperty|bf|xdmg\");\n            commonTests.assert_MISSING(\"itemproperty|s|phb\");\n\n            commonTests.assert_MISSING(\"itemtype|$c|phb\");\n            commonTests.assert_Present(\"itemtype|$c|xphb\");\n            commonTests.assert_MISSING(\"itemtype|$g|dmg\");\n            commonTests.assert_MISSING(\"itemtype|$g|xdmg\");\n            commonTests.assert_MISSING(\"itemtype|sc|dmg\");\n            commonTests.assert_Present(\"itemtype|sc|xphb\");\n            commonTests.assert_MISSING(\"itemtypeadditionalentries|gs|phb|xge\");\n\n            commonTests.assert_MISSING(\"item|+1 rod of the pact keeper|dmg\");\n            commonTests.assert_MISSING(\"item|+1 rod of the pact keeper|xdmg\");\n            commonTests.assert_MISSING(\"item|+2 wraps of unarmed power|xdmg\");\n            commonTests.assert_MISSING(\"item|+2 wraps of unarmed prowess|bmt\");\n            commonTests.assert_MISSING(\"item|acid (vial)|phb\");\n            commonTests.assert_MISSING(\"item|acid absorbing tattoo|tce\");\n            commonTests.assert_Present(\"item|acid|xphb\");\n            commonTests.assert_MISSING(\"item|alchemist's doom|scc\");\n            commonTests.assert_MISSING(\"item|alchemist's fire (flask)|phb\");\n            commonTests.assert_Present(\"item|alchemist's fire|xphb\");\n            commonTests.assert_MISSING(\"item|alchemist's supplies|phb\");\n            commonTests.assert_Present(\"item|alchemist's supplies|xphb\");\n            commonTests.assert_MISSING(\"item|amulet of health|dmg\");\n            commonTests.assert_Present(\"item|amulet of health|xdmg\");\n            commonTests.assert_MISSING(\"item|amulet of proof against detection and location|dmg\");\n            commonTests.assert_Present(\"item|amulet of proof against detection and location|xdmg\");\n            commonTests.assert_MISSING(\"item|armor of invulnerability|dmg\");\n            commonTests.assert_Present(\"item|armor of invulnerability|xdmg\");\n            commonTests.assert_MISSING(\"item|automatic pistol|dmg\");\n            commonTests.assert_MISSING(\"item|automatic rifle|dmg\");\n            commonTests.assert_MISSING(\"item|automatic rifle|xdmg\");\n            commonTests.assert_MISSING(\"item|ball bearings (bag of 1,000)|phb\");\n            commonTests.assert_Present(\"item|ball bearings|xphb\");\n            commonTests.assert_MISSING(\"item|ball bearing|phb\");\n            commonTests.assert_MISSING(\"item|chain (10 feet)|phb\");\n            commonTests.assert_Present(\"item|chain|xphb\");\n\n            commonTests.assert_MISSING(\"monster|abjurer wizard|mpmm\");\n            commonTests.assert_MISSING(\"monster|abjurer|vgm\");\n            commonTests.assert_MISSING(\"monster|alkilith|mpmm\");\n            commonTests.assert_MISSING(\"monster|alkilith|mtf\");\n            commonTests.assert_MISSING(\"monster|animated object (huge)|phb\");\n            commonTests.assert_MISSING(\"monster|ape|mm\");\n            commonTests.assert_Present(\"monster|ape|xmm\");\n            commonTests.assert_MISSING(\"monster|ash zombie|lmop\");\n            commonTests.assert_MISSING(\"monster|ash zombie|pabtso\");\n            commonTests.assert_MISSING(\"monster|awakened shrub|mm\");\n            commonTests.assert_Present(\"monster|awakened shrub|xmm\");\n            commonTests.assert_MISSING(\"monster|beast of the land|tce\");\n            commonTests.assert_MISSING(\"monster|beast of the land|xphb\");\n            commonTests.assert_MISSING(\"monster|bestial spirit (air)|tce\");\n            commonTests.assert_MISSING(\"monster|bestial spirit (air)|xphb\");\n            commonTests.assert_MISSING(\"monster|cat|mm\");\n            commonTests.assert_Present(\"monster|cat|xmm\");\n            commonTests.assert_MISSING(\"monster|derro savant|mpmm\");\n            commonTests.assert_MISSING(\"monster|derro savant|mtf\");\n            commonTests.assert_MISSING(\"monster|derro savant|oota\");\n            commonTests.assert_MISSING(\"monster|sibriex|mpmm\");\n            commonTests.assert_MISSING(\"monster|sibriex|mtf\");\n\n            commonTests.assert_MISSING(\"object|trebuchet|dmg\");\n            commonTests.assert_MISSING(\"object|trebuchet|xdmg\");\n\n            commonTests.assert_MISSING(\"optfeature|ambush|tce\");\n            commonTests.assert_MISSING(\"optfeature|ambush|xphb\");\n            commonTests.assert_MISSING(\"optfeature|dueling|phb\");\n            commonTests.assert_MISSING(\"optfeature|investment of the chain master|tce\");\n            commonTests.assert_Present(\"optfeature|investment of the chain master|xphb\");\n\n            commonTests.assert_MISSING(\"reward|blessing of weapon enhancement|dmg\");\n            commonTests.assert_MISSING(\"reward|blessing of weapon enhancement|xdmg\");\n            commonTests.assert_MISSING(\"reward|blessing of wound closure|dmg\");\n            commonTests.assert_MISSING(\"reward|blessing of wound closure|xdmg\");\n            commonTests.assert_MISSING(\"reward|boon of combat prowess|dmg\");\n            commonTests.assert_MISSING(\"reward|boon of dimensional travel|dmg\");\n            commonTests.assert_MISSING(\"reward|boon of fate|dmg\");\n            commonTests.assert_MISSING(\"reward|boon of fortitude|dmg\");\n            commonTests.assert_MISSING(\"reward|boon of high magic|dmg\");\n\n            commonTests.assert_MISSING(\"sense|blindsight|phb\");\n            commonTests.assert_Present(\"sense|blindsight|xphb\");\n\n            commonTests.assert_MISSING(\"skill|athletics|phb\");\n            commonTests.assert_Present(\"skill|athletics|xphb\");\n\n            commonTests.assert_MISSING(\"spell|acid splash|phb\");\n            commonTests.assert_Present(\"spell|acid splash|xphb\");\n            commonTests.assert_MISSING(\"spell|aganazzar's scorcher|xge\");\n            commonTests.assert_MISSING(\"spell|blade barrier|phb\");\n            commonTests.assert_Present(\"spell|blade barrier|xphb\");\n            commonTests.assert_MISSING(\"spell|feeblemind|phb\");\n            commonTests.assert_MISSING(\"spell|illusory dragon|xge\");\n            commonTests.assert_MISSING(\"spell|illusory script|phb\");\n            commonTests.assert_Present(\"spell|illusory script|xphb\");\n            commonTests.assert_MISSING(\"spell|wrath of nature|xge\");\n\n            commonTests.assert_MISSING(\"status|surprised|phb\");\n            commonTests.assert_Present(\"status|surprised|xphb\");\n\n            commonTests.assert_MISSING(\"trap|collapsing roof|dmg\");\n            commonTests.assert_Present(\"trap|collapsing roof|xdmg\");\n            commonTests.assert_MISSING(\"trap|falling net|dmg\");\n            commonTests.assert_Present(\"trap|falling net|xdmg\");\n            commonTests.assert_MISSING(\"trap|pits|dmg\");\n            commonTests.assert_MISSING(\"trap|poison darts|dmg\");\n            commonTests.assert_Present(\"trap|poisoned darts|xdmg\");\n            commonTests.assert_MISSING(\"trap|poison needle trap|xge\");\n            commonTests.assert_MISSING(\"trap|poison needle|dmg\");\n            commonTests.assert_MISSING(\"trap|rolling sphere|dmg\");\n            commonTests.assert_Present(\"trap|rolling stone|xdmg\");\n\n            commonTests.assert_MISSING(\"vehicle|apparatus of kwalish|dmg\");\n            commonTests.assert_Present(\"vehicle|apparatus of kwalish|xdmg\");\n\n            // Classes, subclasses, class features, and subclass features\n\n            // 2024 free rules define barbarian, bard, cleric, druid, fighter, monk,\n            //    paladin, ranger, rogue, sorcerer, warlock\n            // 2014 basic rules define barbarian, bard, cleric, druid, fighter, monk,\n            //    paladin, ranger, rogue, sorcerer, warlock\n            // 5.1 SRD defines: barbarian, bard, cleric, druid, fighter, monk,\n            //    paladin, ranger, rogue, sorcerer, warlock.\n\n            commonTests.assert_MISSING(\"classtype|artificer|efa\");\n            commonTests.assert_MISSING(\"classtype|artificer|tce\");\n\n            // \"Path of Wild Magic|Barbarian||Wild Magic|TCE|3\",\n            // \"Bolstering Magic|Barbarian||Wild Magic|TCE|6\",\n            // \"Unstable Backlash|Barbarian||Wild Magic|TCE|10\",\n            // \"Controlled Surge|Barbarian||Wild Magic|TCE|14\",\n\n            commonTests.assert_MISSING(\"classtype|barbarian|phb\");\n            commonTests.assert_Present(\"classtype|barbarian|xphb\");\n\n            commonTests.assert_MISSING(\"subclass|path of wild magic|barbarian|phb|tce\");\n            commonTests.assert_MISSING(\"subclass|path of wild magic|barbarian|xphb|tce\");\n\n            commonTests.assert_MISSING(\"subclassfeature|bolstering magic|barbarian|phb|wild magic|tce|6|tce\");\n            commonTests.assert_MISSING(\"subclassfeature|controlled surge|barbarian|phb|wild magic|tce|14|tce\");\n            commonTests.assert_MISSING(\"subclassfeature|magic awareness|barbarian|phb|wild magic|tce|3|tce\");\n            commonTests.assert_MISSING(\"subclassfeature|path of wild magic|barbarian|phb|wild magic|tce|3|tce\");\n            commonTests.assert_MISSING(\"subclassfeature|unstable backlash|barbarian|phb|wild magic|tce|10|tce\");\n            commonTests.assert_MISSING(\"subclassfeature|wild surge|barbarian|phb|wild magic|tce|3|tce\");\n\n            // \"Thief|Rogue||Thief||3\",\n            // \"Supreme Sneak|Rogue||Thief||9\",\n            // \"Use Magic Device|Rogue||Thief||13\",\n            // \"Thief's Reflexes|Rogue||Thief||17\"\n\n            commonTests.assert_MISSING(\"classtype|rogue|phb\");\n            commonTests.assert_Present(\"classtype|rogue|xphb\");\n\n            commonTests.assert_MISSING(\"subclass|thief|rogue|phb|phb\");\n            commonTests.assert_MISSING(\"subclass|thief|rogue|xphb|phb\");\n            commonTests.assert_Present(\"subclass|thief|rogue|xphb|xphb\");\n\n            commonTests.assert_MISSING(\"subclassfeature|thief|rogue|phb|thief|phb|3|phb\");\n            commonTests.assert_Present(\"subclassfeature|thief|rogue|xphb|thief|xphb|3|xphb\");\n            commonTests.assert_MISSING(\"subclassfeature|supreme sneak|rogue|phb|thief|phb|9|phb\");\n            commonTests.assert_Present(\"subclassfeature|supreme sneak|rogue|xphb|thief|xphb|9|xphb\");\n            commonTests.assert_MISSING(\"subclassfeature|use magic device|rogue|phb|thief|phb|13|phb\");\n            commonTests.assert_Present(\"subclassfeature|use magic device|rogue|xphb|thief|xphb|13|xphb\");\n            commonTests.assert_MISSING(\"subclassfeature|thief's reflexes|rogue|phb|thief|phb|17|phb\");\n            commonTests.assert_Present(\"subclassfeature|thief's reflexes|rogue|xphb|thief|xphb|17|xphb\");\n\n            // Races and subraces\n\n            commonTests.assert_MISSING(\"race|bugbear|erlw\");\n            commonTests.assert_MISSING(\"race|bugbear|mpmm\");\n            commonTests.assert_MISSING(\"race|bugbear|vgm\");\n            commonTests.assert_MISSING(\"race|human|phb\");\n            commonTests.assert_Present(\"race|human|xphb\");\n            commonTests.assert_MISSING(\"race|tiefling|phb\");\n            commonTests.assert_Present(\"race|tiefling|xphb\");\n            commonTests.assert_MISSING(\"race|warforged|efa\");\n            commonTests.assert_MISSING(\"race|warforged|erlw\");\n            commonTests.assert_MISSING(\"race|yuan-ti pureblood|vgm\");\n            commonTests.assert_MISSING(\"race|yuan-ti|mpmm\");\n\n            commonTests.assert_MISSING(\"subrace|genasi (air)|genasi|eepc|eepc\");\n            commonTests.assert_MISSING(\"subrace|genasi (air)|genasi|mpmm|mpmm\");\n            commonTests.assert_MISSING(\"subrace|human|human|phb|phb\");\n            commonTests.assert_MISSING(\"subrace|tiefling (zariel)|tiefling|phb|mtf\");\n            commonTests.assert_MISSING(\"subrace|tiefling|tiefling|phb|phb\");\n            commonTests.assert_MISSING(\"subrace|vampire (ixalan)|vampire|psz|psx\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2014Test.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.io.IOException;\nimport java.nio.file.Path;\n\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\nimport dev.ebullient.convert.TestUtils;\nimport dev.ebullient.convert.tools.dnd5e.CommonDataTests.TestInput;\nimport io.quarkus.test.junit.QuarkusTest;\n\n@QuarkusTest\npublic class FilterSrd2014Test {\n\n    static CommonDataTests commonTests;\n    static final TestInput testInput = TestInput.srd2014;\n    static final Path outputPath = TestUtils.OUTPUT_5E_DATA.resolve(testInput.name());\n\n    @BeforeAll\n    public static void setupDir() throws Exception {\n        outputPath.toFile().mkdirs();\n        String config = \"\"\"\n                {\n                    \"sources\": {\n                        \"reference\": [\"srd\", \"basicrules\"]\n                    },\n                    \"images\": {\n                        \"copyInternal\": false\n                    }\n                }\n                \"\"\".stripIndent();\n        commonTests = new CommonDataTests(testInput, config, TestUtils.PATH_5E_TOOLS_DATA);\n    }\n\n    @AfterAll\n    public static void done() throws IOException {\n        commonTests.afterAll(outputPath);\n    }\n\n    @AfterEach\n    public void cleanup() throws Exception {\n        commonTests.afterEach();\n    }\n\n    @Test\n    public void testKeyIndex() throws Exception {\n        commonTests.testKeyIndex(outputPath);\n\n        if (commonTests.dataPresent) {\n            var config = commonTests.config;\n\n            assertThat(config.sourceIncluded(\"srd\")).isTrue();\n            assertThat(config.sourceIncluded(\"basicrules\")).isTrue();\n            assertThat(config.sourceIncluded(\"srd52\")).isFalse();\n            assertThat(config.sourceIncluded(\"basicRules2024\")).isFalse();\n\n            assertThat(config.sourceIncluded(\"DMG\")).isFalse();\n            assertThat(config.sourceIncluded(\"PHB\")).isFalse();\n\n            assertThat(config.sourceIncluded(\"XDMG\")).isFalse();\n            assertThat(config.sourceIncluded(\"XPHB\")).isFalse();\n\n            commonTests.assert_Present(\"action|attack|phb\");\n            commonTests.assert_MISSING(\"action|attack|xphb\");\n            commonTests.assert_Present(\"action|cast a spell|phb\");\n            commonTests.assert_Present(\"action|disengage|phb\");\n            commonTests.assert_MISSING(\"action|disengage|xphb\");\n\n            commonTests.assert_MISSING(\"feat|alert|phb\");\n            commonTests.assert_MISSING(\"feat|alert|xphb\");\n            commonTests.assert_MISSING(\"feat|dueling|xphb\");\n            commonTests.assert_Present(\"feat|grappler|phb\");\n            commonTests.assert_MISSING(\"feat|grappler|xphb\");\n            commonTests.assert_MISSING(\"feat|mobile|phb\");\n            commonTests.assert_MISSING(\"feat|moderately armored|phb\");\n            commonTests.assert_MISSING(\"feat|moderately armored|xphb\");\n\n            commonTests.assert_MISSING(\"variantrule|facing|dmg\");\n            commonTests.assert_MISSING(\"variantrule|falling|xge\");\n            commonTests.assert_MISSING(\"variantrule|familiars|mm\");\n            commonTests.assert_MISSING(\"variantrule|simultaneous effects|xge\");\n            commonTests.assert_MISSING(\"variantrule|simultaneous effects|xphb\");\n\n            commonTests.assert_Present(\"background|sage|phb\");\n            commonTests.assert_MISSING(\"background|sage|xphb\");\n            commonTests.assert_MISSING(\"background|baldur's gate acolyte|bgdia\");\n\n            commonTests.assert_Present(\"condition|blinded|phb\");\n            commonTests.assert_MISSING(\"condition|blinded|xphb\");\n\n            commonTests.assert_MISSING(\"deity|auril|faerûnian|frhof\");\n            commonTests.assert_MISSING(\"deity|auril|faerûnian|scag\");\n            commonTests.assert_Present(\"deity|auril|forgotten realms|phb\");\n            commonTests.assert_MISSING(\"deity|chemosh|dragonlance|dsotdq\");\n            commonTests.assert_MISSING(\"deity|chemosh|dragonlance|phb\");\n            commonTests.assert_MISSING(\"deity|ehlonna|greyhawk|phb\");\n            commonTests.assert_MISSING(\"deity|ehlonna|greyhawk|xdmg\");\n            commonTests.assert_MISSING(\"deity|gruumsh|dawn war|dmg\");\n            commonTests.assert_MISSING(\"deity|gruumsh|exandria|egw\");\n            commonTests.assert_MISSING(\"deity|gruumsh|nonhuman|phb\");\n            commonTests.assert_MISSING(\"deity|gruumsh|orc|scag\");\n            commonTests.assert_MISSING(\"deity|gruumsh|orc|vgm\");\n            commonTests.assert_MISSING(\"deity|the traveler|eberron|erlw\");\n            commonTests.assert_MISSING(\"deity|the traveler|eberron|phb\");\n            commonTests.assert_MISSING(\"deity|the traveler|exandria|egw\");\n\n            commonTests.assert_Present(\"disease|cackle fever|dmg\"); // srd/basicrules\n            commonTests.assert_MISSING(\"disease|cackle fever|xdmg\");\n\n            commonTests.assert_MISSING(\"hazard|quicksand pit|xdmg\");\n            commonTests.assert_MISSING(\"hazard|quicksand|dmg\");\n            commonTests.assert_MISSING(\"hazard|razorvine|dmg\");\n            commonTests.assert_MISSING(\"hazard|razorvine|xdmg\");\n\n            commonTests.assert_MISSING(\"itemgroup|arcane focus|phb\");\n            commonTests.assert_MISSING(\"itemgroup|arcane focus|xphb\");\n            commonTests.assert_MISSING(\"itemgroup|carpet of flying|dmg\");\n            commonTests.assert_MISSING(\"itemgroup|carpet of flying|xdmg\");\n            commonTests.assert_MISSING(\"itemgroup|ioun stone|dmg\");\n            commonTests.assert_MISSING(\"itemgroup|ioun stone|llk\");\n            commonTests.assert_MISSING(\"itemgroup|ioun stone|xdmg\");\n            commonTests.assert_Present(\"itemgroup|musical instrument|phb\");\n            commonTests.assert_MISSING(\"itemgroup|musical instrument|scag\");\n            commonTests.assert_MISSING(\"itemgroup|musical instrument|xphb\");\n            commonTests.assert_MISSING(\"itemgroup|spell scroll|dmg\");\n            commonTests.assert_MISSING(\"itemgroup|spell scroll|xdmg\");\n\n            commonTests.assert_Present(\"itemproperty|2h|phb\");\n            commonTests.assert_MISSING(\"itemproperty|2h|xphb\");\n            commonTests.assert_MISSING(\"itemproperty|bf|dmg\");\n            commonTests.assert_MISSING(\"itemproperty|bf|xdmg\");\n            commonTests.assert_Present(\"itemproperty|s|phb\");\n\n            commonTests.assert_Present(\"itemtype|$c|phb\");\n            commonTests.assert_MISSING(\"itemtype|$c|xphb\");\n            commonTests.assert_MISSING(\"itemtype|$g|dmg\");\n            commonTests.assert_MISSING(\"itemtype|$g|xdmg\");\n            commonTests.assert_MISSING(\"itemtype|sc|dmg\");\n            commonTests.assert_MISSING(\"itemtype|sc|xphb\");\n            commonTests.assert_MISSING(\"itemtypeadditionalentries|gs|phb|xge\");\n\n            commonTests.assert_MISSING(\"item|+1 rod of the pact keeper|dmg\");\n            commonTests.assert_MISSING(\"item|+1 rod of the pact keeper|xdmg\");\n            commonTests.assert_MISSING(\"item|+2 wraps of unarmed power|xdmg\");\n            commonTests.assert_MISSING(\"item|+2 wraps of unarmed prowess|bmt\");\n            commonTests.assert_Present(\"item|acid (vial)|phb\");\n            commonTests.assert_MISSING(\"item|acid absorbing tattoo|tce\");\n            commonTests.assert_MISSING(\"item|acid|xphb\");\n            commonTests.assert_MISSING(\"item|alchemist's doom|scc\");\n            commonTests.assert_Present(\"item|alchemist's fire (flask)|phb\");\n            commonTests.assert_MISSING(\"item|alchemist's fire|xphb\");\n            commonTests.assert_Present(\"item|alchemist's supplies|phb\");\n            commonTests.assert_MISSING(\"item|alchemist's supplies|xphb\");\n            commonTests.assert_Present(\"item|amulet of health|dmg\");\n            commonTests.assert_MISSING(\"item|amulet of health|xdmg\");\n            commonTests.assert_Present(\"item|amulet of proof against detection and location|dmg\");\n            commonTests.assert_MISSING(\"item|amulet of proof against detection and location|xdmg\");\n            commonTests.assert_Present(\"item|armor of invulnerability|dmg\");\n            commonTests.assert_MISSING(\"item|armor of invulnerability|xdmg\");\n            commonTests.assert_MISSING(\"item|automatic pistol|dmg\");\n            commonTests.assert_MISSING(\"item|automatic rifle|dmg\");\n            commonTests.assert_MISSING(\"item|automatic rifle|xdmg\");\n            commonTests.assert_Present(\"item|ball bearings (bag of 1,000)|phb\");\n            commonTests.assert_MISSING(\"item|ball bearings|xphb\");\n            commonTests.assert_Present(\"item|ball bearing|phb\");\n            commonTests.assert_Present(\"item|chain (10 feet)|phb\");\n            commonTests.assert_MISSING(\"item|chain|xphb\");\n\n            commonTests.assert_MISSING(\"monster|abjurer wizard|mpmm\");\n            commonTests.assert_MISSING(\"monster|abjurer|vgm\");\n            commonTests.assert_MISSING(\"monster|alkilith|mpmm\");\n            commonTests.assert_MISSING(\"monster|alkilith|mtf\");\n            commonTests.assert_MISSING(\"monster|animated object (huge)|phb\");\n            commonTests.assert_Present(\"monster|ape|mm\");\n            commonTests.assert_MISSING(\"monster|ape|xmm\");\n            commonTests.assert_MISSING(\"monster|ash zombie|lmop\");\n            commonTests.assert_MISSING(\"monster|ash zombie|pabtso\");\n            commonTests.assert_Present(\"monster|awakened shrub|mm\");\n            commonTests.assert_MISSING(\"monster|awakened shrub|xmm\");\n            commonTests.assert_MISSING(\"monster|beast of the land|tce\");\n            commonTests.assert_MISSING(\"monster|beast of the land|xphb\");\n            commonTests.assert_MISSING(\"monster|bestial spirit (air)|tce\");\n            commonTests.assert_MISSING(\"monster|bestial spirit (air)|xphb\");\n            commonTests.assert_Present(\"monster|cat|mm\");\n            commonTests.assert_MISSING(\"monster|cat|xmm\");\n            commonTests.assert_MISSING(\"monster|derro savant|mpmm\");\n            commonTests.assert_MISSING(\"monster|derro savant|mtf\");\n            commonTests.assert_MISSING(\"monster|derro savant|oota\");\n            commonTests.assert_MISSING(\"monster|sibriex|mpmm\");\n            commonTests.assert_MISSING(\"monster|sibriex|mtf\");\n\n            commonTests.assert_MISSING(\"object|trebuchet|dmg\");\n            commonTests.assert_MISSING(\"object|trebuchet|xdmg\");\n\n            commonTests.assert_MISSING(\"optfeature|ambush|tce\");\n            commonTests.assert_MISSING(\"optfeature|ambush|xphb\");\n            commonTests.assert_MISSING(\"optfeature|investment of the chain master|tce\");\n            commonTests.assert_MISSING(\"optfeature|investment of the chain master|xphb\");\n\n            commonTests.assert_MISSING(\"reward|blessing of weapon enhancement|dmg\");\n            commonTests.assert_MISSING(\"reward|blessing of weapon enhancement|xdmg\");\n            commonTests.assert_MISSING(\"reward|blessing of wound closure|dmg\");\n            commonTests.assert_MISSING(\"reward|blessing of wound closure|xdmg\");\n            commonTests.assert_MISSING(\"reward|boon of combat prowess|dmg\");\n            commonTests.assert_MISSING(\"reward|boon of dimensional travel|dmg\");\n            commonTests.assert_MISSING(\"reward|boon of fate|dmg\");\n            commonTests.assert_MISSING(\"reward|boon of fortitude|dmg\");\n            commonTests.assert_MISSING(\"reward|boon of high magic|dmg\");\n\n            commonTests.assert_Present(\"sense|blindsight|phb\");\n            commonTests.assert_MISSING(\"sense|blindsight|xphb\");\n\n            commonTests.assert_Present(\"skill|athletics|phb\");\n            commonTests.assert_MISSING(\"skill|athletics|xphb\");\n\n            commonTests.assert_Present(\"spell|acid splash|phb\");\n            commonTests.assert_MISSING(\"spell|acid splash|xphb\");\n            commonTests.assert_MISSING(\"spell|aganazzar's scorcher|xge\");\n            commonTests.assert_Present(\"spell|blade barrier|phb\");\n            commonTests.assert_MISSING(\"spell|blade barrier|xphb\");\n            commonTests.assert_Present(\"spell|feeblemind|phb\");\n            commonTests.assert_MISSING(\"spell|illusory dragon|xge\");\n            commonTests.assert_Present(\"spell|illusory script|phb\");\n            commonTests.assert_MISSING(\"spell|illusory script|xphb\");\n            commonTests.assert_MISSING(\"spell|wrath of nature|xge\");\n\n            commonTests.assert_Present(\"status|surprised|phb\");\n            commonTests.assert_MISSING(\"status|surprised|xphb\");\n\n            commonTests.assert_Present(\"trap|collapsing roof|dmg\");\n            commonTests.assert_MISSING(\"trap|collapsing roof|xdmg\");\n            commonTests.assert_Present(\"trap|falling net|dmg\");\n            commonTests.assert_MISSING(\"trap|falling net|xdmg\");\n            commonTests.assert_Present(\"trap|pits|dmg\");\n            commonTests.assert_MISSING(\"trap|hidden pit|xdmg\");\n            commonTests.assert_Present(\"trap|poison darts|dmg\");\n            commonTests.assert_MISSING(\"trap|poisoned darts|xdmg\");\n            commonTests.assert_MISSING(\"trap|poison needle trap|xge\");\n            commonTests.assert_Present(\"trap|poison needle|dmg\");\n            commonTests.assert_Present(\"trap|rolling sphere|dmg\");\n            commonTests.assert_MISSING(\"trap|rolling stone|xdmg\");\n\n            commonTests.assert_Present(\"vehicle|apparatus of kwalish|dmg\");\n            commonTests.assert_MISSING(\"vehicle|apparatus of kwalish|xdmg\");\n\n            // Classes, subclasses, class features, and subclass features\n\n            commonTests.assert_MISSING(\"classtype|artificer|efa\");\n            commonTests.assert_MISSING(\"classtype|artificer|tce\");\n\n            // \"Path of Wild Magic|Barbarian||Wild Magic|TCE|3\",\n            // \"Bolstering Magic|Barbarian||Wild Magic|TCE|6\",\n            // \"Unstable Backlash|Barbarian||Wild Magic|TCE|10\",\n            // \"Controlled Surge|Barbarian||Wild Magic|TCE|14\",\n\n            commonTests.assert_Present(\"classtype|barbarian|phb\");\n            commonTests.assert_MISSING(\"classtype|barbarian|xphb\");\n\n            commonTests.assert_MISSING(\"subclass|path of wild magic|barbarian|phb|tce\");\n            commonTests.assert_MISSING(\"subclass|path of wild magic|barbarian|xphb|tce\");\n\n            commonTests.assert_MISSING(\"subclassfeature|bolstering magic|barbarian|phb|wild magic|tce|6|tce\");\n            commonTests.assert_MISSING(\"subclassfeature|controlled surge|barbarian|phb|wild magic|tce|14|tce\");\n            commonTests.assert_MISSING(\"subclassfeature|magic awareness|barbarian|phb|wild magic|tce|3|tce\");\n            commonTests.assert_MISSING(\"subclassfeature|path of wild magic|barbarian|phb|wild magic|tce|3|tce\");\n            commonTests.assert_MISSING(\"subclassfeature|unstable backlash|barbarian|phb|wild magic|tce|10|tce\");\n            commonTests.assert_MISSING(\"subclassfeature|wild surge|barbarian|phb|wild magic|tce|3|tce\");\n\n            // \"Thief|Rogue||Thief||3\",\n            // \"Supreme Sneak|Rogue||Thief||9\",\n            // \"Use Magic Device|Rogue||Thief||13\",\n            // \"Thief's Reflexes|Rogue||Thief||17\"\n\n            commonTests.assert_Present(\"classtype|rogue|phb\");\n            commonTests.assert_MISSING(\"classtype|rogue|xphb\");\n\n            commonTests.assert_Present(\"subclass|thief|rogue|phb|phb\");\n            commonTests.assert_MISSING(\"subclass|thief|rogue|xphb|phb\");\n            commonTests.assert_MISSING(\"subclass|thief|rogue|xphb|xphb\");\n\n            commonTests.assert_Present(\"subclassfeature|thief|rogue|phb|thief|phb|3|phb\");\n            commonTests.assert_MISSING(\"subclassfeature|thief|rogue|xphb|thief|xphb|3|xphb\");\n            commonTests.assert_Present(\"subclassfeature|supreme sneak|rogue|phb|thief|phb|9|phb\");\n            commonTests.assert_MISSING(\"subclassfeature|supreme sneak|rogue|xphb|thief|xphb|9|xphb\");\n            commonTests.assert_Present(\"subclassfeature|use magic device|rogue|phb|thief|phb|13|phb\");\n            commonTests.assert_MISSING(\"subclassfeature|use magic device|rogue|xphb|thief|xphb|13|xphb\");\n            commonTests.assert_Present(\"subclassfeature|thief's reflexes|rogue|phb|thief|phb|17|phb\");\n            commonTests.assert_MISSING(\"subclassfeature|thief's reflexes|rogue|xphb|thief|xphb|17|xphb\");\n\n            // Races and subraces\n\n            commonTests.assert_MISSING(\"race|bugbear|erlw\");\n            commonTests.assert_MISSING(\"race|bugbear|mpmm\");\n            commonTests.assert_MISSING(\"race|bugbear|vgm\");\n            commonTests.assert_Present(\"race|human|phb\");\n            commonTests.assert_MISSING(\"race|human|xphb\");\n            commonTests.assert_Present(\"race|tiefling|phb\");\n            commonTests.assert_MISSING(\"race|tiefling|xphb\");\n            commonTests.assert_MISSING(\"race|warforged|efa\");\n            commonTests.assert_MISSING(\"race|warforged|erlw\");\n            commonTests.assert_MISSING(\"race|yuan-ti pureblood|vgm\");\n            commonTests.assert_MISSING(\"race|yuan-ti|mpmm\");\n\n            commonTests.assert_MISSING(\"subrace|genasi (air)|genasi|eepc|eepc\");\n            commonTests.assert_MISSING(\"subrace|genasi (air)|genasi|mpmm|mpmm\");\n            commonTests.assert_Present(\"subrace|human|human|phb|phb\");\n            commonTests.assert_MISSING(\"subrace|tiefling (zariel)|tiefling|phb|mtf\");\n            commonTests.assert_Present(\"subrace|tiefling|tiefling|phb|phb\");\n            commonTests.assert_MISSING(\"subrace|vampire (ixalan)|vampire|psz|psx\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrd2024Test.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.io.IOException;\nimport java.nio.file.Path;\n\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\nimport dev.ebullient.convert.TestUtils;\nimport dev.ebullient.convert.tools.dnd5e.CommonDataTests.TestInput;\nimport io.quarkus.test.junit.QuarkusTest;\n\n@QuarkusTest\npublic class FilterSrd2024Test {\n\n    static CommonDataTests commonTests;\n    static final TestInput testInput = TestInput.srd2024;\n    static final Path outputPath = TestUtils.OUTPUT_5E_DATA.resolve(testInput.name());\n\n    @BeforeAll\n    public static void setupDir() throws Exception {\n        outputPath.toFile().mkdirs();\n        String config = \"\"\"\n                {\n                    \"sources\": {\n                        \"reference\": [\"srd52\", \"basicRules2024\"]\n                    },\n                    \"images\": {\n                        \"copyInternal\": false\n                    }\n                }\n                \"\"\".stripIndent();\n        commonTests = new CommonDataTests(testInput, config, TestUtils.PATH_5E_TOOLS_DATA);\n    }\n\n    @AfterAll\n    public static void done() throws IOException {\n        commonTests.afterAll(outputPath);\n    }\n\n    @AfterEach\n    public void cleanup() throws Exception {\n        commonTests.afterEach();\n    }\n\n    @Test\n    public void testKeyIndex() throws Exception {\n        commonTests.testKeyIndex(outputPath);\n\n        if (commonTests.dataPresent) {\n            var config = commonTests.config;\n\n            assertThat(config.sourceIncluded(\"srd\")).isFalse();\n            assertThat(config.sourceIncluded(\"basicrules\")).isFalse();\n            assertThat(config.sourceIncluded(\"srd52\")).isTrue();\n            assertThat(config.sourceIncluded(\"basicRules2024\")).isTrue();\n\n            assertThat(config.sourceIncluded(\"DMG\")).isFalse();\n            assertThat(config.sourceIncluded(\"PHB\")).isFalse();\n\n            assertThat(config.sourceIncluded(\"XDMG\")).isFalse();\n            assertThat(config.sourceIncluded(\"XPHB\")).isFalse();\n\n            commonTests.assert_MISSING(\"action|attack|phb\");\n            commonTests.assert_Present(\"action|attack|xphb\");\n            commonTests.assert_MISSING(\"action|cast a spell|phb\");\n            commonTests.assert_MISSING(\"action|disengage|phb\");\n            commonTests.assert_Present(\"action|disengage|xphb\");\n\n            commonTests.assert_MISSING(\"feat|alert|phb\");\n            commonTests.assert_Present(\"feat|alert|xphb\");\n            commonTests.assert_MISSING(\"feat|dueling|xphb\");\n            commonTests.assert_MISSING(\"feat|grappler|phb\");\n            commonTests.assert_Present(\"feat|grappler|xphb\");\n            commonTests.assert_MISSING(\"feat|mobile|phb\");\n            commonTests.assert_MISSING(\"feat|moderately armored|phb\");\n            commonTests.assert_MISSING(\"feat|moderately armored|xphb\");\n\n            commonTests.assert_MISSING(\"variantrule|facing|dmg\");\n            commonTests.assert_MISSING(\"variantrule|falling|xge\");\n            commonTests.assert_MISSING(\"variantrule|familiars|mm\");\n            commonTests.assert_MISSING(\"variantrule|simultaneous effects|xge\");\n            commonTests.assert_Present(\"variantrule|simultaneous effects|xphb\");\n\n            commonTests.assert_MISSING(\"background|sage|phb\");\n            commonTests.assert_Present(\"background|sage|xphb\");\n            commonTests.assert_MISSING(\"background|baldur's gate acolyte|bgdia\");\n\n            commonTests.assert_MISSING(\"condition|blinded|phb\");\n            commonTests.assert_Present(\"condition|blinded|xphb\");\n\n            commonTests.assert_MISSING(\"deity|auril|faerûnian|frhof\");\n            commonTests.assert_MISSING(\"deity|auril|faerûnian|scag\");\n            commonTests.assert_MISSING(\"deity|auril|forgotten realms|phb\");\n            commonTests.assert_MISSING(\"deity|chemosh|dragonlance|dsotdq\");\n            commonTests.assert_MISSING(\"deity|chemosh|dragonlance|phb\");\n            commonTests.assert_MISSING(\"deity|ehlonna|greyhawk|phb\");\n            commonTests.assert_MISSING(\"deity|ehlonna|greyhawk|xdmg\");\n            commonTests.assert_MISSING(\"deity|gruumsh|dawn war|dmg\");\n            commonTests.assert_MISSING(\"deity|gruumsh|exandria|egw\");\n            commonTests.assert_MISSING(\"deity|gruumsh|nonhuman|phb\");\n            commonTests.assert_MISSING(\"deity|gruumsh|orc|scag\");\n            commonTests.assert_MISSING(\"deity|gruumsh|orc|vgm\");\n            commonTests.assert_MISSING(\"deity|the traveler|eberron|erlw\");\n            commonTests.assert_MISSING(\"deity|the traveler|eberron|phb\");\n            commonTests.assert_MISSING(\"deity|the traveler|exandria|egw\");\n\n            commonTests.assert_MISSING(\"disease|cackle fever|dmg\");\n            commonTests.assert_Present(\"disease|cackle fever|xdmg\");\n\n            commonTests.assert_MISSING(\"hazard|quicksand pit|xdmg\");\n            commonTests.assert_MISSING(\"hazard|quicksand|dmg\");\n            commonTests.assert_MISSING(\"hazard|razorvine|dmg\");\n            commonTests.assert_MISSING(\"hazard|razorvine|xdmg\");\n\n            commonTests.assert_MISSING(\"itemgroup|arcane focus|phb\");\n            commonTests.assert_MISSING(\"itemgroup|arcane focus|xphb\");\n            commonTests.assert_MISSING(\"itemgroup|carpet of flying|dmg\");\n            commonTests.assert_Present(\"itemgroup|carpet of flying|xdmg\");\n            commonTests.assert_MISSING(\"itemgroup|ioun stone|dmg\");\n            commonTests.assert_MISSING(\"itemgroup|ioun stone|llk\");\n            commonTests.assert_Present(\"itemgroup|ioun stone|xdmg\");\n            commonTests.assert_MISSING(\"itemgroup|musical instrument|phb\");\n            commonTests.assert_MISSING(\"itemgroup|musical instrument|scag\");\n            commonTests.assert_Present(\"itemgroup|musical instrument|xphb\");\n            commonTests.assert_MISSING(\"itemgroup|spell scroll|dmg\");\n            commonTests.assert_Present(\"itemgroup|spell scroll|xdmg\");\n\n            commonTests.assert_MISSING(\"itemproperty|2h|phb\");\n            commonTests.assert_Present(\"itemproperty|2h|xphb\");\n            commonTests.assert_MISSING(\"itemproperty|bf|dmg\");\n            commonTests.assert_MISSING(\"itemproperty|bf|xdmg\");\n            commonTests.assert_MISSING(\"itemproperty|s|phb\");\n\n            commonTests.assert_MISSING(\"itemtype|$c|phb\");\n            commonTests.assert_Present(\"itemtype|$c|xphb\");\n            commonTests.assert_MISSING(\"itemtype|$g|dmg\");\n            commonTests.assert_MISSING(\"itemtype|$g|xdmg\");\n            commonTests.assert_MISSING(\"itemtype|sc|dmg\");\n            commonTests.assert_Present(\"itemtype|sc|xphb\");\n            commonTests.assert_MISSING(\"itemtypeadditionalentries|gs|phb|xge\");\n\n            commonTests.assert_MISSING(\"item|+1 rod of the pact keeper|dmg\");\n            commonTests.assert_MISSING(\"item|+1 rod of the pact keeper|xdmg\");\n            commonTests.assert_MISSING(\"item|+2 wraps of unarmed power|xdmg\");\n            commonTests.assert_MISSING(\"item|+2 wraps of unarmed prowess|bmt\");\n            commonTests.assert_MISSING(\"item|acid (vial)|phb\");\n            commonTests.assert_MISSING(\"item|acid absorbing tattoo|tce\");\n            commonTests.assert_Present(\"item|acid|xphb\");\n            commonTests.assert_MISSING(\"item|alchemist's doom|scc\");\n            commonTests.assert_MISSING(\"item|alchemist's fire (flask)|phb\");\n            commonTests.assert_Present(\"item|alchemist's fire|xphb\");\n            commonTests.assert_MISSING(\"item|alchemist's supplies|phb\");\n            commonTests.assert_Present(\"item|alchemist's supplies|xphb\");\n            commonTests.assert_MISSING(\"item|amulet of health|dmg\");\n            commonTests.assert_Present(\"item|amulet of health|xdmg\");\n            commonTests.assert_MISSING(\"item|amulet of proof against detection and location|dmg\");\n            commonTests.assert_Present(\"item|amulet of proof against detection and location|xdmg\");\n            commonTests.assert_MISSING(\"item|armor of invulnerability|dmg\");\n            commonTests.assert_Present(\"item|armor of invulnerability|xdmg\");\n            commonTests.assert_MISSING(\"item|automatic pistol|dmg\");\n            commonTests.assert_MISSING(\"item|automatic rifle|dmg\");\n            commonTests.assert_MISSING(\"item|automatic rifle|xdmg\");\n            commonTests.assert_MISSING(\"item|ball bearings (bag of 1,000)|phb\");\n            commonTests.assert_Present(\"item|ball bearings|xphb\");\n            commonTests.assert_MISSING(\"item|ball bearing|phb\");\n            commonTests.assert_MISSING(\"item|chain (10 feet)|phb\");\n            commonTests.assert_Present(\"item|chain|xphb\");\n\n            commonTests.assert_MISSING(\"monster|abjurer wizard|mpmm\");\n            commonTests.assert_MISSING(\"monster|abjurer|vgm\");\n            commonTests.assert_MISSING(\"monster|alkilith|mpmm\");\n            commonTests.assert_MISSING(\"monster|alkilith|mtf\");\n            commonTests.assert_MISSING(\"monster|animated object (huge)|phb\");\n            commonTests.assert_MISSING(\"monster|ape|mm\");\n            commonTests.assert_Present(\"monster|ape|xmm\");\n            commonTests.assert_MISSING(\"monster|ash zombie|lmop\");\n            commonTests.assert_MISSING(\"monster|ash zombie|pabtso\");\n            commonTests.assert_MISSING(\"monster|awakened shrub|mm\");\n            commonTests.assert_Present(\"monster|awakened shrub|xmm\");\n            commonTests.assert_MISSING(\"monster|beast of the land|tce\");\n            commonTests.assert_MISSING(\"monster|beast of the land|xphb\");\n            commonTests.assert_MISSING(\"monster|bestial spirit (air)|tce\");\n            commonTests.assert_MISSING(\"monster|bestial spirit (air)|xphb\");\n            commonTests.assert_MISSING(\"monster|cat|mm\");\n            commonTests.assert_Present(\"monster|cat|xmm\");\n            commonTests.assert_MISSING(\"monster|derro savant|mpmm\");\n            commonTests.assert_MISSING(\"monster|derro savant|mtf\");\n            commonTests.assert_MISSING(\"monster|derro savant|oota\");\n            commonTests.assert_MISSING(\"monster|sibriex|mpmm\");\n            commonTests.assert_MISSING(\"monster|sibriex|mtf\");\n\n            commonTests.assert_MISSING(\"object|trebuchet|dmg\");\n            commonTests.assert_MISSING(\"object|trebuchet|xdmg\");\n\n            commonTests.assert_MISSING(\"optfeature|ambush|tce\");\n            commonTests.assert_MISSING(\"optfeature|ambush|xphb\");\n            commonTests.assert_MISSING(\"optfeature|investment of the chain master|tce\");\n            commonTests.assert_Present(\"optfeature|investment of the chain master|xphb\");\n\n            commonTests.assert_MISSING(\"reward|blessing of weapon enhancement|dmg\");\n            commonTests.assert_MISSING(\"reward|blessing of weapon enhancement|xdmg\");\n            commonTests.assert_MISSING(\"reward|blessing of wound closure|dmg\");\n            commonTests.assert_MISSING(\"reward|blessing of wound closure|xdmg\");\n            commonTests.assert_MISSING(\"reward|boon of combat prowess|dmg\");\n            commonTests.assert_MISSING(\"reward|boon of dimensional travel|dmg\");\n            commonTests.assert_MISSING(\"reward|boon of fate|dmg\");\n            commonTests.assert_MISSING(\"reward|boon of fortitude|dmg\");\n            commonTests.assert_MISSING(\"reward|boon of high magic|dmg\");\n\n            commonTests.assert_MISSING(\"sense|blindsight|phb\");\n            commonTests.assert_Present(\"sense|blindsight|xphb\");\n\n            commonTests.assert_MISSING(\"skill|athletics|phb\");\n            commonTests.assert_Present(\"skill|athletics|xphb\");\n\n            commonTests.assert_MISSING(\"spell|acid splash|phb\");\n            commonTests.assert_Present(\"spell|acid splash|xphb\");\n            commonTests.assert_MISSING(\"spell|aganazzar's scorcher|xge\");\n            commonTests.assert_MISSING(\"spell|blade barrier|phb\");\n            commonTests.assert_Present(\"spell|blade barrier|xphb\");\n            commonTests.assert_MISSING(\"spell|feeblemind|phb\");\n            commonTests.assert_MISSING(\"spell|illusory dragon|xge\");\n            commonTests.assert_MISSING(\"spell|illusory script|phb\");\n            commonTests.assert_Present(\"spell|illusory script|xphb\");\n            commonTests.assert_MISSING(\"spell|wrath of nature|xge\");\n\n            commonTests.assert_MISSING(\"status|surprised|phb\");\n            commonTests.assert_Present(\"status|surprised|xphb\");\n\n            commonTests.assert_MISSING(\"trap|collapsing roof|dmg\");\n            commonTests.assert_Present(\"trap|collapsing roof|xdmg\"); // basicRules2024\n            commonTests.assert_MISSING(\"trap|falling net|dmg\");\n            commonTests.assert_Present(\"trap|falling net|xdmg\"); // basicRules2024\n            commonTests.assert_MISSING(\"trap|pits|dmg\");\n            commonTests.assert_Present(\"trap|hidden pit|xdmg\"); // basicRules2024\n            commonTests.assert_MISSING(\"trap|poison darts|dmg\");\n            commonTests.assert_Present(\"trap|poisoned darts|xdmg\"); // basicRules2024\n            commonTests.assert_MISSING(\"trap|poison needle trap|xge\");\n            commonTests.assert_MISSING(\"trap|poison needle|dmg\");\n            commonTests.assert_Present(\"trap|poisoned darts|xdmg\"); // basicRules2024\n            commonTests.assert_MISSING(\"trap|rolling sphere|dmg\");\n            commonTests.assert_Present(\"trap|rolling stone|xdmg\"); // basicRules2024\n\n            commonTests.assert_MISSING(\"vehicle|apparatus of kwalish|dmg\");\n            commonTests.assert_Present(\"vehicle|apparatus of kwalish|xdmg\");\n\n            // Classes, subclasses, class features, and subclass features\n\n            commonTests.assert_MISSING(\"classtype|artificer|efa\");\n            commonTests.assert_MISSING(\"classtype|artificer|tce\");\n\n            // \"Path of Wild Magic|Barbarian||Wild Magic|TCE|3\",\n            // \"Bolstering Magic|Barbarian||Wild Magic|TCE|6\",\n            // \"Unstable Backlash|Barbarian||Wild Magic|TCE|10\",\n            // \"Controlled Surge|Barbarian||Wild Magic|TCE|14\",\n\n            commonTests.assert_MISSING(\"classtype|barbarian|phb\");\n            commonTests.assert_Present(\"classtype|barbarian|xphb\");\n\n            commonTests.assert_MISSING(\"subclass|path of wild magic|barbarian|phb|tce\");\n            commonTests.assert_MISSING(\"subclass|path of wild magic|barbarian|xphb|tce\");\n\n            commonTests.assert_MISSING(\"subclassfeature|bolstering magic|barbarian|phb|wild magic|tce|6|tce\");\n            commonTests.assert_MISSING(\"subclassfeature|controlled surge|barbarian|phb|wild magic|tce|14|tce\");\n            commonTests.assert_MISSING(\"subclassfeature|magic awareness|barbarian|phb|wild magic|tce|3|tce\");\n            commonTests.assert_MISSING(\"subclassfeature|path of wild magic|barbarian|phb|wild magic|tce|3|tce\");\n            commonTests.assert_MISSING(\"subclassfeature|unstable backlash|barbarian|phb|wild magic|tce|10|tce\");\n            commonTests.assert_MISSING(\"subclassfeature|wild surge|barbarian|phb|wild magic|tce|3|tce\");\n\n            // \"Thief|Rogue||Thief||3\",\n            // \"Supreme Sneak|Rogue||Thief||9\",\n            // \"Use Magic Device|Rogue||Thief||13\",\n            // \"Thief's Reflexes|Rogue||Thief||17\"\n\n            commonTests.assert_MISSING(\"classtype|rogue|phb\");\n            commonTests.assert_Present(\"classtype|rogue|xphb\");\n\n            commonTests.assert_MISSING(\"subclass|thief|rogue|phb|phb\");\n            commonTests.assert_MISSING(\"subclass|thief|rogue|xphb|phb\");\n            commonTests.assert_Present(\"subclass|thief|rogue|xphb|xphb\");\n\n            commonTests.assert_MISSING(\"subclassfeature|thief|rogue|phb|thief|phb|3|phb\");\n            commonTests.assert_Present(\"subclassfeature|thief|rogue|xphb|thief|xphb|3|xphb\");\n            commonTests.assert_MISSING(\"subclassfeature|supreme sneak|rogue|phb|thief|phb|9|phb\");\n            commonTests.assert_Present(\"subclassfeature|supreme sneak|rogue|xphb|thief|xphb|9|xphb\");\n            commonTests.assert_MISSING(\"subclassfeature|use magic device|rogue|phb|thief|phb|13|phb\");\n            commonTests.assert_Present(\"subclassfeature|use magic device|rogue|xphb|thief|xphb|13|xphb\");\n            commonTests.assert_MISSING(\"subclassfeature|thief's reflexes|rogue|phb|thief|phb|17|phb\");\n            commonTests.assert_Present(\"subclassfeature|thief's reflexes|rogue|xphb|thief|xphb|17|xphb\");\n\n            // Races and subraces\n\n            commonTests.assert_MISSING(\"race|bugbear|erlw\");\n            commonTests.assert_MISSING(\"race|bugbear|mpmm\");\n            commonTests.assert_MISSING(\"race|bugbear|vgm\");\n            commonTests.assert_MISSING(\"race|human|phb\");\n            commonTests.assert_Present(\"race|human|xphb\");\n            commonTests.assert_MISSING(\"race|tiefling|phb\");\n            commonTests.assert_Present(\"race|tiefling|xphb\");\n            commonTests.assert_MISSING(\"race|warforged|efa\");\n            commonTests.assert_MISSING(\"race|warforged|erlw\");\n            commonTests.assert_MISSING(\"race|yuan-ti pureblood|vgm\");\n            commonTests.assert_MISSING(\"race|yuan-ti|mpmm\");\n\n            commonTests.assert_MISSING(\"subrace|genasi (air)|genasi|eepc|eepc\");\n            commonTests.assert_MISSING(\"subrace|genasi (air)|genasi|mpmm|mpmm\");\n            commonTests.assert_MISSING(\"subrace|human|human|phb|phb\");\n            commonTests.assert_MISSING(\"subrace|tiefling (zariel)|tiefling|phb|mtf\");\n            commonTests.assert_MISSING(\"subrace|tiefling|tiefling|phb|phb\");\n            commonTests.assert_MISSING(\"subrace|vampire (ixalan)|vampire|psz|psx\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSrdEditionsTest.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.io.IOException;\nimport java.nio.file.Path;\n\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\nimport dev.ebullient.convert.TestUtils;\nimport dev.ebullient.convert.tools.dnd5e.CommonDataTests.TestInput;\nimport io.quarkus.test.junit.QuarkusTest;\n\n@QuarkusTest\npublic class FilterSrdEditionsTest {\n\n    static CommonDataTests commonTests;\n    static final TestInput testInput = TestInput.srdEdition;\n    static final Path outputPath = TestUtils.OUTPUT_5E_DATA.resolve(testInput.name());\n\n    @BeforeAll\n    public static void setupDir() throws Exception {\n        outputPath.toFile().mkdirs();\n        String config = \"\"\"\n                {\n                    \"sources\": {\n                        \"reference\": [\"srd\", \"basicrules\", \"srd52\", \"basicRules2024\"]\n                    },\n                    \"reprintBehavior\": \"edition\",\n                    \"images\": {\n                        \"copyInternal\": false\n                    }\n                }\n                \"\"\".stripIndent();\n        commonTests = new CommonDataTests(testInput, config, TestUtils.PATH_5E_TOOLS_DATA);\n    }\n\n    @AfterAll\n    public static void done() throws IOException {\n        commonTests.afterAll(outputPath);\n    }\n\n    @AfterEach\n    public void cleanup() throws Exception {\n        commonTests.afterEach();\n    }\n\n    @Test\n    public void testKeyIndex() throws Exception {\n        commonTests.testKeyIndex(outputPath);\n\n        // NONE: resouces from 2024 freerules, 5.1 SRD, and 2014 basic rules\n        // without following reprints across editions.\n        // (2014 content will remain, alongside 2024 content)\n\n        if (commonTests.dataPresent) {\n            var config = commonTests.config;\n\n            assertThat(config.noSources()).isFalse();\n            assertThat(Tools5eIndex.isSrdBasicOnly()).isTrue();\n\n            assertThat(config.sourceIncluded(\"srd\")).isTrue();\n            assertThat(config.sourceIncluded(\"basicrules\")).isTrue();\n            assertThat(config.sourceIncluded(\"srd52\")).isTrue();\n            assertThat(config.sourceIncluded(\"basicRules2024\")).isTrue();\n\n            assertThat(config.sourceIncluded(\"DMG\")).isFalse();\n            assertThat(config.sourceIncluded(\"PHB\")).isFalse();\n            assertThat(config.sourceIncluded(\"MM\")).isFalse();\n\n            assertThat(config.sourceIncluded(\"XDMG\")).isFalse();\n            assertThat(config.sourceIncluded(\"XPHB\")).isFalse();\n            assertThat(config.sourceIncluded(\"XMM\")).isFalse();\n\n            commonTests.assert_MISSING(\"action|attack|phb\");\n            commonTests.assert_Present(\"action|attack|xphb\");\n            commonTests.assert_MISSING(\"action|cast a spell|phb\");\n            commonTests.assert_MISSING(\"action|disengage|phb\");\n            commonTests.assert_Present(\"action|disengage|xphb\");\n\n            commonTests.assert_MISSING(\"feat|alert|phb\");\n            commonTests.assert_Present(\"feat|alert|xphb\");\n            commonTests.assert_MISSING(\"feat|dueling|xphb\");\n            commonTests.assert_MISSING(\"feat|grappler|phb\");\n            commonTests.assert_Present(\"feat|grappler|xphb\");\n            commonTests.assert_MISSING(\"feat|mobile|phb\");\n            commonTests.assert_MISSING(\"feat|moderately armored|phb\");\n            commonTests.assert_MISSING(\"feat|moderately armored|xphb\");\n\n            commonTests.assert_MISSING(\"variantrule|facing|dmg\");\n            commonTests.assert_MISSING(\"variantrule|falling|xge\");\n            commonTests.assert_MISSING(\"variantrule|familiars|mm\");\n            commonTests.assert_MISSING(\"variantrule|simultaneous effects|xge\");\n            commonTests.assert_Present(\"variantrule|simultaneous effects|xphb\");\n\n            commonTests.assert_Present(\"background|sage|phb\");\n            commonTests.assert_Present(\"background|sage|xphb\");\n            commonTests.assert_MISSING(\"background|baldur's gate acolyte|bgdia\");\n\n            commonTests.assert_MISSING(\"condition|blinded|phb\");\n            commonTests.assert_Present(\"condition|blinded|xphb\");\n            commonTests.assert_Present(\"condition|blinded|xphb\");\n\n            commonTests.assert_MISSING(\"deity|auril|faerûnian|frhof\");\n            commonTests.assert_MISSING(\"deity|auril|faerûnian|scag\");\n            commonTests.assert_Present(\"deity|auril|forgotten realms|phb\"); // part of basic rules/srd\n            commonTests.assert_MISSING(\"deity|chemosh|dragonlance|dsotdq\");\n            commonTests.assert_MISSING(\"deity|chemosh|dragonlance|phb\");\n            commonTests.assert_MISSING(\"deity|ehlonna|greyhawk|phb\");\n            commonTests.assert_MISSING(\"deity|ehlonna|greyhawk|xdmg\");\n            commonTests.assert_MISSING(\"deity|gruumsh|dawn war|dmg\");\n            commonTests.assert_MISSING(\"deity|gruumsh|exandria|egw\");\n            commonTests.assert_MISSING(\"deity|gruumsh|nonhuman|phb\");\n            commonTests.assert_MISSING(\"deity|gruumsh|orc|scag\");\n            commonTests.assert_MISSING(\"deity|gruumsh|orc|vgm\");\n            commonTests.assert_MISSING(\"deity|the traveler|eberron|erlw\");\n            commonTests.assert_MISSING(\"deity|the traveler|eberron|phb\");\n            commonTests.assert_MISSING(\"deity|the traveler|exandria|egw\");\n\n            commonTests.assert_MISSING(\"disease|cackle fever|dmg\"); // part of basic rules\n            commonTests.assert_Present(\"disease|cackle fever|xdmg\");\n\n            commonTests.assert_MISSING(\"hazard|quicksand pit|xdmg\");\n            commonTests.assert_MISSING(\"hazard|quicksand|dmg\");\n            commonTests.assert_MISSING(\"hazard|razorvine|dmg\");\n            commonTests.assert_MISSING(\"hazard|razorvine|xdmg\");\n\n            commonTests.assert_MISSING(\"itemgroup|arcane focus|phb\");\n            commonTests.assert_MISSING(\"itemgroup|arcane focus|xphb\");\n            commonTests.assert_MISSING(\"itemgroup|carpet of flying|dmg\");\n            commonTests.assert_Present(\"itemgroup|carpet of flying|xdmg\");\n            commonTests.assert_MISSING(\"itemgroup|ioun stone|dmg\");\n            commonTests.assert_MISSING(\"itemgroup|ioun stone|llk\");\n            commonTests.assert_Present(\"itemgroup|ioun stone|xdmg\");\n            commonTests.assert_MISSING(\"itemgroup|musical instrument|phb\");\n            commonTests.assert_MISSING(\"itemgroup|musical instrument|scag\");\n            commonTests.assert_Present(\"itemgroup|musical instrument|xphb\");\n            commonTests.assert_MISSING(\"itemgroup|spell scroll|dmg\");\n            commonTests.assert_Present(\"itemgroup|spell scroll|xdmg\");\n\n            commonTests.assert_MISSING(\"itemproperty|2h|phb\");\n            commonTests.assert_Present(\"itemproperty|2h|xphb\");\n            commonTests.assert_MISSING(\"itemproperty|bf|dmg\");\n            commonTests.assert_MISSING(\"itemproperty|bf|xdmg\");\n            commonTests.assert_Present(\"itemproperty|s|phb\");\n\n            commonTests.assert_MISSING(\"itemtype|$c|phb\");\n            commonTests.assert_Present(\"itemtype|$c|xphb\");\n            commonTests.assert_MISSING(\"itemtype|$g|dmg\");\n            commonTests.assert_MISSING(\"itemtype|$g|xdmg\");\n            commonTests.assert_MISSING(\"itemtype|sc|dmg\");\n            commonTests.assert_Present(\"itemtype|sc|xphb\");\n            commonTests.assert_MISSING(\"itemtypeadditionalentries|gs|phb|xge\");\n\n            commonTests.assert_MISSING(\"item|+1 rod of the pact keeper|dmg\");\n            commonTests.assert_MISSING(\"item|+1 rod of the pact keeper|xdmg\");\n            commonTests.assert_MISSING(\"item|+2 wraps of unarmed power|xdmg\");\n            commonTests.assert_MISSING(\"item|+2 wraps of unarmed prowess|bmt\");\n            commonTests.assert_MISSING(\"item|acid (vial)|phb\");\n            commonTests.assert_MISSING(\"item|acid absorbing tattoo|tce\");\n            commonTests.assert_Present(\"item|acid|xphb\");\n            commonTests.assert_MISSING(\"item|alchemist's doom|scc\");\n            commonTests.assert_MISSING(\"item|alchemist's fire (flask)|phb\");\n            commonTests.assert_Present(\"item|alchemist's fire|xphb\");\n            commonTests.assert_MISSING(\"item|alchemist's supplies|phb\");\n            commonTests.assert_Present(\"item|alchemist's supplies|xphb\");\n            commonTests.assert_MISSING(\"item|amulet of health|dmg\");\n            commonTests.assert_Present(\"item|amulet of health|xdmg\");\n            commonTests.assert_MISSING(\"item|amulet of proof against detection and location|dmg\");\n            commonTests.assert_Present(\"item|amulet of proof against detection and location|xdmg\");\n            commonTests.assert_MISSING(\"item|armor of invulnerability|dmg\");\n            commonTests.assert_Present(\"item|armor of invulnerability|xdmg\");\n            commonTests.assert_MISSING(\"item|automatic pistol|dmg\");\n            commonTests.assert_MISSING(\"item|automatic rifle|dmg\");\n            commonTests.assert_MISSING(\"item|automatic rifle|xdmg\");\n            commonTests.assert_MISSING(\"item|ball bearings (bag of 1,000)|phb\");\n            commonTests.assert_Present(\"item|ball bearings|xphb\");\n            commonTests.assert_Present(\"item|ball bearing|phb\");\n            commonTests.assert_MISSING(\"item|chain (10 feet)|phb\");\n            commonTests.assert_Present(\"item|chain|xphb\");\n\n            commonTests.assert_MISSING(\"monster|abjurer wizard|mpmm\");\n            commonTests.assert_MISSING(\"monster|abjurer|vgm\");\n            commonTests.assert_MISSING(\"monster|alkilith|mpmm\");\n            commonTests.assert_MISSING(\"monster|alkilith|mtf\");\n            commonTests.assert_MISSING(\"monster|animated object (huge)|phb\");\n            commonTests.assert_MISSING(\"monster|ape|mm\");\n            commonTests.assert_Present(\"monster|ape|xmm\");\n            commonTests.assert_MISSING(\"monster|ash zombie|lmop\");\n            commonTests.assert_MISSING(\"monster|ash zombie|pabtso\");\n            commonTests.assert_MISSING(\"monster|awakened shrub|mm\");\n            commonTests.assert_Present(\"monster|awakened shrub|xmm\");\n            commonTests.assert_MISSING(\"monster|beast of the land|tce\");\n            commonTests.assert_MISSING(\"monster|beast of the land|xphb\");\n            commonTests.assert_MISSING(\"monster|bestial spirit (air)|tce\");\n            commonTests.assert_MISSING(\"monster|bestial spirit (air)|xphb\");\n            commonTests.assert_MISSING(\"monster|cat|mm\");\n            commonTests.assert_Present(\"monster|cat|xmm\");\n            commonTests.assert_MISSING(\"monster|derro savant|mpmm\");\n            commonTests.assert_MISSING(\"monster|derro savant|mtf\");\n            commonTests.assert_MISSING(\"monster|derro savant|oota\");\n            commonTests.assert_MISSING(\"monster|sibriex|mpmm\");\n            commonTests.assert_MISSING(\"monster|sibriex|mtf\");\n\n            commonTests.assert_MISSING(\"object|trebuchet|dmg\");\n            commonTests.assert_MISSING(\"object|trebuchet|xdmg\");\n\n            commonTests.assert_MISSING(\"optfeature|ambush|tce\");\n            commonTests.assert_MISSING(\"optfeature|ambush|xphb\");\n            commonTests.assert_Present(\"optfeature|dueling|phb\");\n            commonTests.assert_MISSING(\"optfeature|investment of the chain master|tce\");\n            commonTests.assert_Present(\"optfeature|investment of the chain master|xphb\");\n\n            commonTests.assert_MISSING(\"reward|blessing of weapon enhancement|dmg\");\n            commonTests.assert_MISSING(\"reward|blessing of weapon enhancement|xdmg\");\n            commonTests.assert_MISSING(\"reward|blessing of wound closure|dmg\");\n            commonTests.assert_MISSING(\"reward|blessing of wound closure|xdmg\");\n            commonTests.assert_MISSING(\"reward|boon of combat prowess|dmg\");\n            commonTests.assert_MISSING(\"reward|boon of dimensional travel|dmg\");\n            commonTests.assert_MISSING(\"reward|boon of fate|dmg\");\n            commonTests.assert_MISSING(\"reward|boon of fortitude|dmg\");\n            commonTests.assert_MISSING(\"reward|boon of high magic|dmg\");\n\n            commonTests.assert_MISSING(\"sense|blindsight|phb\");\n            commonTests.assert_Present(\"sense|blindsight|xphb\");\n\n            commonTests.assert_MISSING(\"skill|athletics|phb\");\n            commonTests.assert_Present(\"skill|athletics|xphb\");\n\n            commonTests.assert_MISSING(\"spell|acid splash|phb\");\n            commonTests.assert_Present(\"spell|acid splash|xphb\");\n            commonTests.assert_MISSING(\"spell|aganazzar's scorcher|xge\");\n            commonTests.assert_MISSING(\"spell|blade barrier|phb\");\n            commonTests.assert_Present(\"spell|blade barrier|xphb\");\n            commonTests.assert_MISSING(\"spell|feeblemind|phb\");\n            commonTests.assert_MISSING(\"spell|illusory dragon|xge\");\n            commonTests.assert_MISSING(\"spell|illusory script|phb\");\n            commonTests.assert_Present(\"spell|illusory script|xphb\");\n            commonTests.assert_MISSING(\"spell|wrath of nature|xge\");\n\n            commonTests.assert_MISSING(\"status|surprised|phb\");\n            commonTests.assert_Present(\"status|surprised|xphb\");\n\n            commonTests.assert_MISSING(\"trap|collapsing roof|dmg\");\n            commonTests.assert_Present(\"trap|collapsing roof|xdmg\"); // basicRules2024\n            commonTests.assert_MISSING(\"trap|falling net|dmg\");\n            commonTests.assert_Present(\"trap|falling net|xdmg\"); // basicRules2024\n            commonTests.assert_MISSING(\"trap|pits|dmg\");\n            commonTests.assert_Present(\"trap|hidden pit|xdmg\"); // basicRules2024\n            commonTests.assert_MISSING(\"trap|poison darts|dmg\");\n            commonTests.assert_Present(\"trap|poisoned darts|xdmg\"); // basicRules2024\n            commonTests.assert_MISSING(\"trap|poison needle trap|xge\");\n            commonTests.assert_MISSING(\"trap|poison needle|dmg\");\n            commonTests.assert_Present(\"trap|poisoned needle|xdmg\"); // basicRules2024\n            commonTests.assert_MISSING(\"trap|rolling sphere|dmg\");\n            commonTests.assert_Present(\"trap|rolling stone|xdmg\"); // basicRules2024\n\n            commonTests.assert_MISSING(\"vehicle|apparatus of kwalish|dmg\");\n            commonTests.assert_Present(\"vehicle|apparatus of kwalish|xdmg\"); // rd52 and basicRules2024\n\n            // Classes, subclasses, class features, and subclass features\n\n            commonTests.assert_MISSING(\"classtype|artificer|efa\");\n            commonTests.assert_MISSING(\"classtype|artificer|tce\");\n\n            // \"Path of Wild Magic|Barbarian||Wild Magic|TCE|3\",\n            // \"Bolstering Magic|Barbarian||Wild Magic|TCE|6\",\n            // \"Unstable Backlash|Barbarian||Wild Magic|TCE|10\",\n            // \"Controlled Surge|Barbarian||Wild Magic|TCE|14\",\n\n            commonTests.assert_Present(\"classtype|barbarian|phb\");\n            commonTests.assert_Present(\"classtype|barbarian|xphb\");\n\n            commonTests.assert_MISSING(\"subclass|path of wild magic|barbarian|phb|tce\");\n            commonTests.assert_MISSING(\"subclass|path of wild magic|barbarian|xphb|tce\");\n\n            commonTests.assert_MISSING(\"subclassfeature|bolstering magic|barbarian|phb|wild magic|tce|6|tce\");\n            commonTests.assert_MISSING(\"subclassfeature|controlled surge|barbarian|phb|wild magic|tce|14|tce\");\n            commonTests.assert_MISSING(\"subclassfeature|magic awareness|barbarian|phb|wild magic|tce|3|tce\");\n            commonTests.assert_MISSING(\"subclassfeature|path of wild magic|barbarian|phb|wild magic|tce|3|tce\");\n            commonTests.assert_MISSING(\"subclassfeature|unstable backlash|barbarian|phb|wild magic|tce|10|tce\");\n            commonTests.assert_MISSING(\"subclassfeature|wild surge|barbarian|phb|wild magic|tce|3|tce\");\n\n            // \"Thief|Rogue||Thief||3\",\n            // \"Supreme Sneak|Rogue||Thief||9\",\n            // \"Use Magic Device|Rogue||Thief||13\",\n            // \"Thief's Reflexes|Rogue||Thief||17\"\n\n            commonTests.assert_Present(\"classtype|rogue|phb\");\n            commonTests.assert_Present(\"classtype|rogue|xphb\");\n\n            commonTests.assert_Present(\"subclass|thief|rogue|phb|phb\");\n            commonTests.assert_MISSING(\"subclass|thief|rogue|xphb|phb\");\n            commonTests.assert_Present(\"subclass|thief|rogue|xphb|xphb\");\n\n            commonTests.assert_Present(\"subclassfeature|thief|rogue|phb|thief|phb|3|phb\");\n            commonTests.assert_Present(\"subclassfeature|thief|rogue|xphb|thief|xphb|3|xphb\");\n            commonTests.assert_Present(\"subclassfeature|supreme sneak|rogue|phb|thief|phb|9|phb\");\n            commonTests.assert_Present(\"subclassfeature|supreme sneak|rogue|xphb|thief|xphb|9|xphb\");\n            commonTests.assert_Present(\"subclassfeature|use magic device|rogue|phb|thief|phb|13|phb\");\n            commonTests.assert_Present(\"subclassfeature|use magic device|rogue|xphb|thief|xphb|13|xphb\");\n            commonTests.assert_Present(\"subclassfeature|thief's reflexes|rogue|phb|thief|phb|17|phb\");\n            commonTests.assert_Present(\"subclassfeature|thief's reflexes|rogue|xphb|thief|xphb|17|xphb\");\n\n            // Races and subraces\n\n            commonTests.assert_MISSING(\"race|bugbear|erlw\");\n            commonTests.assert_MISSING(\"race|bugbear|mpmm\");\n            commonTests.assert_MISSING(\"race|bugbear|vgm\");\n            commonTests.assert_Present(\"race|human|phb\");\n            commonTests.assert_Present(\"race|human|xphb\");\n            commonTests.assert_Present(\"race|tiefling|phb\"); // in srd\n            commonTests.assert_Present(\"race|tiefling|xphb\");\n            commonTests.assert_MISSING(\"race|warforged|efa\");\n            commonTests.assert_MISSING(\"race|warforged|erlw\");\n            commonTests.assert_MISSING(\"race|yuan-ti pureblood|vgm\");\n            commonTests.assert_MISSING(\"race|yuan-ti|mpmm\");\n\n            commonTests.assert_MISSING(\"subrace|genasi (air)|genasi|eepc|eepc\");\n            commonTests.assert_MISSING(\"subrace|genasi (air)|genasi|mpmm|mpmm\");\n            commonTests.assert_Present(\"subrace|human|human|phb|phb\");\n            commonTests.assert_MISSING(\"subrace|tiefling (zariel)|tiefling|phb|mtf\");\n            commonTests.assert_Present(\"subrace|tiefling|tiefling|phb|phb\"); // srd\n            commonTests.assert_MISSING(\"subrace|vampire (ixalan)|vampire|psz|psx\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2014Test.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.io.IOException;\nimport java.nio.file.Path;\n\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\nimport dev.ebullient.convert.TestUtils;\nimport dev.ebullient.convert.tools.dnd5e.CommonDataTests.TestInput;\nimport io.quarkus.test.junit.QuarkusTest;\n\n@QuarkusTest\npublic class FilterSubset2014Test {\n\n    static CommonDataTests commonTests;\n    static final TestInput testInput = TestInput.subset2014;\n    static final Path outputPath = TestUtils.OUTPUT_5E_DATA.resolve(testInput.name());\n\n    @BeforeAll\n    public static void setupDir() throws Exception {\n        outputPath.toFile().mkdirs();\n        String config = \"\"\"\n                {\n                    \"sources\": {\n                        \"adventure\": [\"lmop\"],\n                        \"book\": [\"phb\", \"dmg\", \"mm\"],\n                        \"reference\": [\"tce\", \"xge\"]\n                    },\n                    \"images\": {\n                        \"copyInternal\": false\n                    }\n                }\n                \"\"\".stripIndent();\n        commonTests = new CommonDataTests(testInput, config, TestUtils.PATH_5E_TOOLS_DATA);\n    }\n\n    @AfterAll\n    public static void done() throws IOException {\n        commonTests.afterAll(outputPath);\n    }\n\n    @AfterEach\n    public void cleanup() throws Exception {\n        commonTests.afterEach();\n    }\n\n    @Test\n    public void testKeyIndex() throws Exception {\n        commonTests.testKeyIndex(outputPath);\n\n        if (commonTests.dataPresent) {\n            var config = commonTests.config;\n\n            assertThat(config.sourceIncluded(\"srd\")).isFalse();\n            assertThat(config.sourceIncluded(\"basicrules\")).isFalse();\n            assertThat(config.sourceIncluded(\"srd52\")).isFalse();\n            assertThat(config.sourceIncluded(\"basicRules2024\")).isFalse();\n\n            assertThat(config.sourceIncluded(\"DMG\")).isTrue();\n            assertThat(config.sourceIncluded(\"PHB\")).isTrue();\n\n            assertThat(config.sourceIncluded(\"XDMG\")).isFalse();\n            assertThat(config.sourceIncluded(\"XPHB\")).isFalse();\n\n            assertThat(config.sourceIncluded(\"MM\")).isTrue();\n            assertThat(config.sourceIncluded(\"MPMM\")).isFalse();\n            assertThat(config.sourceIncluded(\"SCAG\")).isFalse();\n            assertThat(config.sourceIncluded(\"TCE\")).isTrue();\n            assertThat(config.sourceIncluded(\"XGE\")).isTrue();\n\n            assertThat(config.sourceIncluded(\"PaBTSO\")).isFalse();\n            assertThat(config.sourceIncluded(\"OotA\")).isFalse();\n            assertThat(config.sourceIncluded(\"LMOP\")).isTrue();\n\n            commonTests.assert_Present(\"action|attack|phb\");\n            commonTests.assert_MISSING(\"action|attack|xphb\");\n            commonTests.assert_Present(\"action|cast a spell|phb\");\n            commonTests.assert_Present(\"action|disengage|phb\");\n            commonTests.assert_MISSING(\"action|disengage|xphb\");\n\n            commonTests.assert_Present(\"feat|alert|phb\");\n            commonTests.assert_MISSING(\"feat|alert|xphb\");\n            commonTests.assert_MISSING(\"feat|dueling|xphb\");\n            commonTests.assert_Present(\"feat|grappler|phb\");\n            commonTests.assert_MISSING(\"feat|grappler|xphb\");\n            commonTests.assert_Present(\"feat|mobile|phb\");\n            commonTests.assert_Present(\"feat|moderately armored|phb\");\n            commonTests.assert_MISSING(\"feat|moderately armored|xphb\");\n\n            commonTests.assert_Present(\"variantrule|facing|dmg\");\n            commonTests.assert_Present(\"variantrule|falling|xge\");\n            commonTests.assert_Present(\"variantrule|familiars|mm\");\n            commonTests.assert_Present(\"variantrule|simultaneous effects|xge\");\n            commonTests.assert_MISSING(\"variantrule|simultaneous effects|xphb\");\n\n            commonTests.assert_Present(\"background|sage|phb\");\n            commonTests.assert_MISSING(\"background|sage|xphb\");\n            commonTests.assert_MISSING(\"background|baldur's gate acolyte|bgdia\");\n\n            commonTests.assert_Present(\"condition|blinded|phb\");\n            commonTests.assert_MISSING(\"condition|blinded|xphb\");\n\n            commonTests.assert_MISSING(\"deity|auril|faerûnian|frhof\");\n            commonTests.assert_MISSING(\"deity|auril|faerûnian|scag\");\n            commonTests.assert_Present(\"deity|auril|forgotten realms|phb\");\n            commonTests.assert_MISSING(\"deity|chemosh|dragonlance|dsotdq\");\n            commonTests.assert_Present(\"deity|chemosh|dragonlance|phb\");\n            commonTests.assert_Present(\"deity|ehlonna|greyhawk|phb\");\n            commonTests.assert_MISSING(\"deity|ehlonna|greyhawk|xdmg\");\n            commonTests.assert_Present(\"deity|gruumsh|dawn war|dmg\");\n            commonTests.assert_MISSING(\"deity|gruumsh|exandria|egw\");\n            commonTests.assert_Present(\"deity|gruumsh|nonhuman|phb\");\n            commonTests.assert_MISSING(\"deity|gruumsh|orc|scag\");\n            commonTests.assert_MISSING(\"deity|gruumsh|orc|vgm\");\n            commonTests.assert_MISSING(\"deity|the traveler|eberron|erlw\");\n            commonTests.assert_Present(\"deity|the traveler|eberron|phb\");\n            commonTests.assert_MISSING(\"deity|the traveler|exandria|egw\");\n\n            commonTests.assert_Present(\"disease|cackle fever|dmg\");\n            commonTests.assert_MISSING(\"disease|cackle fever|xdmg\");\n\n            commonTests.assert_MISSING(\"hazard|quicksand pit|xdmg\");\n            commonTests.assert_Present(\"hazard|quicksand|dmg\");\n            commonTests.assert_Present(\"hazard|razorvine|dmg\");\n            commonTests.assert_MISSING(\"hazard|razorvine|xdmg\");\n\n            commonTests.assert_Present(\"itemgroup|arcane focus|phb\");\n            commonTests.assert_MISSING(\"itemgroup|arcane focus|xphb\");\n            commonTests.assert_Present(\"itemgroup|carpet of flying|dmg\");\n            commonTests.assert_MISSING(\"itemgroup|carpet of flying|xdmg\");\n            commonTests.assert_Present(\"itemgroup|ioun stone|dmg\");\n            commonTests.assert_MISSING(\"itemgroup|ioun stone|llk\");\n            commonTests.assert_MISSING(\"itemgroup|ioun stone|xdmg\");\n            commonTests.assert_Present(\"itemgroup|musical instrument|phb\");\n            commonTests.assert_MISSING(\"itemgroup|musical instrument|scag\");\n            commonTests.assert_MISSING(\"itemgroup|musical instrument|xphb\");\n            commonTests.assert_Present(\"itemgroup|spell scroll|dmg\");\n            commonTests.assert_MISSING(\"itemgroup|spell scroll|xdmg\");\n\n            commonTests.assert_Present(\"itemproperty|2h|phb\");\n            commonTests.assert_MISSING(\"itemproperty|2h|xphb\");\n            commonTests.assert_Present(\"itemproperty|bf|dmg\");\n            commonTests.assert_MISSING(\"itemproperty|bf|xdmg\");\n            commonTests.assert_Present(\"itemproperty|s|phb\");\n\n            commonTests.assert_Present(\"itemtype|$c|phb\");\n            commonTests.assert_MISSING(\"itemtype|$c|xphb\");\n            commonTests.assert_Present(\"itemtype|$g|dmg\");\n            commonTests.assert_MISSING(\"itemtype|$g|xdmg\");\n            commonTests.assert_Present(\"itemtype|sc|dmg\");\n            commonTests.assert_MISSING(\"itemtype|sc|xphb\");\n            commonTests.assert_Present(\"itemtypeadditionalentries|gs|phb|xge\");\n\n            commonTests.assert_Present(\"item|+1 rod of the pact keeper|dmg\");\n            commonTests.assert_MISSING(\"item|+1 rod of the pact keeper|xdmg\");\n            commonTests.assert_MISSING(\"item|+2 wraps of unarmed power|xdmg\");\n            commonTests.assert_MISSING(\"item|+2 wraps of unarmed prowess|bmt\");\n            commonTests.assert_Present(\"item|acid (vial)|phb\");\n            commonTests.assert_Present(\"item|acid absorbing tattoo|tce\");\n            commonTests.assert_MISSING(\"item|acid|xphb\");\n            commonTests.assert_MISSING(\"item|alchemist's doom|scc\");\n            commonTests.assert_Present(\"item|alchemist's fire (flask)|phb\");\n            commonTests.assert_MISSING(\"item|alchemist's fire|xphb\");\n            commonTests.assert_Present(\"item|alchemist's supplies|phb\");\n            commonTests.assert_MISSING(\"item|alchemist's supplies|xphb\");\n            commonTests.assert_Present(\"item|amulet of health|dmg\");\n            commonTests.assert_MISSING(\"item|amulet of health|xdmg\");\n            commonTests.assert_Present(\"item|amulet of proof against detection and location|dmg\");\n            commonTests.assert_MISSING(\"item|amulet of proof against detection and location|xdmg\");\n            commonTests.assert_Present(\"item|armor of invulnerability|dmg\");\n            commonTests.assert_MISSING(\"item|armor of invulnerability|xdmg\");\n            commonTests.assert_Present(\"item|automatic pistol|dmg\");\n            commonTests.assert_Present(\"item|automatic rifle|dmg\");\n            commonTests.assert_MISSING(\"item|automatic rifle|xdmg\");\n            commonTests.assert_Present(\"item|ball bearings (bag of 1,000)|phb\");\n            commonTests.assert_MISSING(\"item|ball bearings|xphb\");\n            commonTests.assert_Present(\"item|ball bearing|phb\");\n            commonTests.assert_Present(\"item|chain (10 feet)|phb\");\n            commonTests.assert_MISSING(\"item|chain|xphb\");\n\n            commonTests.assert_MISSING(\"monster|abjurer wizard|mpmm\");\n            commonTests.assert_MISSING(\"monster|abjurer|vgm\");\n            commonTests.assert_MISSING(\"monster|alkilith|mpmm\");\n            commonTests.assert_MISSING(\"monster|alkilith|mtf\");\n            commonTests.assert_Present(\"monster|animated object (huge)|phb\");\n            commonTests.assert_Present(\"monster|ape|mm\");\n            commonTests.assert_MISSING(\"monster|ape|xmm\");\n            commonTests.assert_Present(\"monster|ash zombie|lmop\");\n            commonTests.assert_MISSING(\"monster|ash zombie|pabtso\");\n            commonTests.assert_Present(\"monster|awakened shrub|mm\");\n            commonTests.assert_MISSING(\"monster|awakened shrub|xmm\");\n            commonTests.assert_Present(\"monster|beast of the land|tce\");\n            commonTests.assert_MISSING(\"monster|beast of the land|xphb\");\n            commonTests.assert_Present(\"monster|bestial spirit (air)|tce\");\n            commonTests.assert_MISSING(\"monster|bestial spirit (air)|xphb\");\n            commonTests.assert_Present(\"monster|cat|mm\");\n            commonTests.assert_MISSING(\"monster|cat|xmm\");\n            commonTests.assert_MISSING(\"monster|derro savant|mpmm\");\n            commonTests.assert_MISSING(\"monster|derro savant|mtf\");\n            commonTests.assert_MISSING(\"monster|derro savant|oota\");\n            commonTests.assert_MISSING(\"monster|sibriex|mpmm\");\n            commonTests.assert_MISSING(\"monster|sibriex|mtf\");\n\n            commonTests.assert_Present(\"object|trebuchet|dmg\");\n            commonTests.assert_MISSING(\"object|trebuchet|xdmg\");\n\n            commonTests.assert_Present(\"optfeature|ambush|tce\");\n            commonTests.assert_MISSING(\"optfeature|ambush|xphb\");\n            commonTests.assert_Present(\"optfeature|investment of the chain master|tce\");\n            commonTests.assert_MISSING(\"optfeature|investment of the chain master|xphb\");\n\n            commonTests.assert_Present(\"reward|blessing of weapon enhancement|dmg\");\n            commonTests.assert_MISSING(\"reward|blessing of weapon enhancement|xdmg\");\n            commonTests.assert_Present(\"reward|blessing of wound closure|dmg\");\n            commonTests.assert_MISSING(\"reward|blessing of wound closure|xdmg\");\n            commonTests.assert_Present(\"reward|boon of combat prowess|dmg\");\n            commonTests.assert_Present(\"reward|boon of dimensional travel|dmg\");\n            commonTests.assert_Present(\"reward|boon of fate|dmg\");\n            commonTests.assert_Present(\"reward|boon of fortitude|dmg\");\n            commonTests.assert_Present(\"reward|boon of high magic|dmg\");\n\n            commonTests.assert_Present(\"sense|blindsight|phb\");\n            commonTests.assert_MISSING(\"sense|blindsight|xphb\");\n\n            commonTests.assert_Present(\"skill|athletics|phb\");\n            commonTests.assert_MISSING(\"skill|athletics|xphb\");\n\n            commonTests.assert_Present(\"spell|acid splash|phb\");\n            commonTests.assert_MISSING(\"spell|acid splash|xphb\");\n            commonTests.assert_Present(\"spell|aganazzar's scorcher|xge\");\n            commonTests.assert_Present(\"spell|blade barrier|phb\");\n            commonTests.assert_MISSING(\"spell|blade barrier|xphb\");\n            commonTests.assert_Present(\"spell|feeblemind|phb\");\n            commonTests.assert_Present(\"spell|illusory dragon|xge\");\n            commonTests.assert_Present(\"spell|illusory script|phb\");\n            commonTests.assert_MISSING(\"spell|illusory script|xphb\");\n            commonTests.assert_Present(\"spell|wrath of nature|xge\");\n\n            commonTests.assert_Present(\"status|surprised|phb\");\n            commonTests.assert_MISSING(\"status|surprised|xphb\");\n\n            commonTests.assert_Present(\"trap|collapsing roof|dmg\");\n            commonTests.assert_MISSING(\"trap|collapsing roof|xdmg\"); // basicRules2024\n            commonTests.assert_Present(\"trap|falling net|dmg\");\n            commonTests.assert_MISSING(\"trap|falling net|xdmg\"); // basicRules2024\n            commonTests.assert_Present(\"trap|pits|dmg\");\n            commonTests.assert_MISSING(\"trap|hidden pit|xdmg\"); // basicRules2024\n            commonTests.assert_Present(\"trap|poison darts|dmg\"); // srd\n            commonTests.assert_MISSING(\"trap|poisoned darts|xdmg\"); // basicRules2024\n            commonTests.assert_Present(\"trap|poison needle trap|xge\");\n            commonTests.assert_Present(\"trap|poison needle|dmg\");\n            commonTests.assert_MISSING(\"trap|poisoned needle|xdmg\"); // basicRules2024\n            commonTests.assert_Present(\"trap|rolling sphere|dmg\");\n            commonTests.assert_MISSING(\"trap|rolling stone|xdmg\");\n\n            commonTests.assert_Present(\"vehicle|apparatus of kwalish|dmg\");\n            commonTests.assert_MISSING(\"vehicle|apparatus of kwalish|xdmg\");\n\n            // Classes, subclasses, class features, and subclass features\n\n            commonTests.assert_MISSING(\"classtype|artificer|efa\");\n            commonTests.assert_Present(\"classtype|artificer|tce\");\n\n            // \"Path of Wild Magic|Barbarian||Wild Magic|TCE|3\",\n            // \"Bolstering Magic|Barbarian||Wild Magic|TCE|6\",\n            // \"Unstable Backlash|Barbarian||Wild Magic|TCE|10\",\n            // \"Controlled Surge|Barbarian||Wild Magic|TCE|14\",\n\n            commonTests.assert_Present(\"classtype|barbarian|phb\");\n            commonTests.assert_MISSING(\"classtype|barbarian|xphb\");\n\n            commonTests.assert_Present(\"subclass|path of wild magic|barbarian|phb|tce\");\n            commonTests.assert_MISSING(\"subclass|path of wild magic|barbarian|xphb|tce\");\n\n            commonTests.assert_Present(\"subclassfeature|bolstering magic|barbarian|phb|wild magic|tce|6|tce\");\n            commonTests.assert_Present(\"subclassfeature|controlled surge|barbarian|phb|wild magic|tce|14|tce\");\n            commonTests.assert_Present(\"subclassfeature|magic awareness|barbarian|phb|wild magic|tce|3|tce\");\n            commonTests.assert_Present(\"subclassfeature|path of wild magic|barbarian|phb|wild magic|tce|3|tce\");\n            commonTests.assert_Present(\"subclassfeature|unstable backlash|barbarian|phb|wild magic|tce|10|tce\");\n            commonTests.assert_Present(\"subclassfeature|wild surge|barbarian|phb|wild magic|tce|3|tce\");\n\n            // \"Thief|Rogue||Thief||3\",\n            // \"Supreme Sneak|Rogue||Thief||9\",\n            // \"Use Magic Device|Rogue||Thief||13\",\n            // \"Thief's Reflexes|Rogue||Thief||17\"\n\n            commonTests.assert_Present(\"classtype|rogue|phb\");\n            commonTests.assert_MISSING(\"classtype|rogue|xphb\");\n\n            commonTests.assert_Present(\"subclass|thief|rogue|phb|phb\");\n            commonTests.assert_MISSING(\"subclass|thief|rogue|xphb|phb\");\n            commonTests.assert_MISSING(\"subclass|thief|rogue|xphb|xphb\");\n\n            commonTests.assert_Present(\"subclassfeature|thief|rogue|phb|thief|phb|3|phb\");\n            commonTests.assert_MISSING(\"subclassfeature|thief|rogue|xphb|thief|xphb|3|xphb\");\n            commonTests.assert_Present(\"subclassfeature|supreme sneak|rogue|phb|thief|phb|9|phb\");\n            commonTests.assert_MISSING(\"subclassfeature|supreme sneak|rogue|xphb|thief|xphb|9|xphb\");\n            commonTests.assert_Present(\"subclassfeature|use magic device|rogue|phb|thief|phb|13|phb\");\n            commonTests.assert_MISSING(\"subclassfeature|use magic device|rogue|xphb|thief|xphb|13|xphb\");\n            commonTests.assert_Present(\"subclassfeature|thief's reflexes|rogue|phb|thief|phb|17|phb\");\n            commonTests.assert_MISSING(\"subclassfeature|thief's reflexes|rogue|xphb|thief|xphb|17|xphb\");\n\n            // Races and subraces\n\n            commonTests.assert_MISSING(\"race|bugbear|erlw\");\n            commonTests.assert_MISSING(\"race|bugbear|mpmm\");\n            commonTests.assert_MISSING(\"race|bugbear|vgm\");\n            commonTests.assert_Present(\"race|human|phb\");\n            commonTests.assert_MISSING(\"race|human|xphb\");\n            commonTests.assert_Present(\"race|tiefling|phb\");\n            commonTests.assert_MISSING(\"race|tiefling|xphb\");\n            commonTests.assert_MISSING(\"race|warforged|efa\");\n            commonTests.assert_MISSING(\"race|warforged|erlw\");\n            commonTests.assert_MISSING(\"race|yuan-ti pureblood|vgm\");\n            commonTests.assert_MISSING(\"race|yuan-ti|mpmm\");\n\n            commonTests.assert_MISSING(\"subrace|genasi (air)|genasi|eepc|eepc\");\n            commonTests.assert_MISSING(\"subrace|genasi (air)|genasi|mpmm|mpmm\");\n            commonTests.assert_Present(\"subrace|human|human|phb|phb\");\n            commonTests.assert_MISSING(\"subrace|tiefling (zariel)|tiefling|phb|mtf\");\n            commonTests.assert_Present(\"subrace|tiefling|tiefling|phb|phb\");\n            commonTests.assert_MISSING(\"subrace|vampire (ixalan)|vampire|psz|psx\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubset2024Test.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.io.IOException;\nimport java.nio.file.Path;\n\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\nimport dev.ebullient.convert.TestUtils;\nimport dev.ebullient.convert.tools.dnd5e.CommonDataTests.TestInput;\nimport io.quarkus.test.junit.QuarkusTest;\n\n@QuarkusTest\npublic class FilterSubset2024Test {\n\n    static CommonDataTests commonTests;\n    static final TestInput testInput = TestInput.subset2024;\n    static final Path outputPath = TestUtils.OUTPUT_5E_DATA.resolve(testInput.name());\n\n    @BeforeAll\n    public static void setupDir() throws Exception {\n        outputPath.toFile().mkdirs();\n        String config = \"\"\"\n                {\n                    \"sources\": {\n                        \"adventure\": [\"PaBTSO\", \"dsotdq\"],\n                        \"book\": [\"xdmg\", \"xphb\"],\n                        \"reference\": [\"srd52\", \"basicRules2024\", \"mpmm\"]\n                    },\n                    \"images\": {\n                        \"copyInternal\": false\n                    }\n                }\n                \"\"\".stripIndent();\n        commonTests = new CommonDataTests(testInput, config, TestUtils.PATH_5E_TOOLS_DATA);\n    }\n\n    @AfterAll\n    public static void done() throws IOException {\n        commonTests.afterAll(outputPath);\n    }\n\n    @AfterEach\n    public void cleanup() throws Exception {\n        commonTests.afterEach();\n    }\n\n    @Test\n    public void testKeyIndex() throws Exception {\n        commonTests.testKeyIndex(outputPath);\n\n        if (commonTests.dataPresent) {\n            var config = commonTests.config;\n\n            assertThat(config.sourceIncluded(\"srd\")).isFalse();\n            assertThat(config.sourceIncluded(\"basicrules\")).isFalse();\n            assertThat(config.sourceIncluded(\"srd52\")).isTrue();\n            assertThat(config.sourceIncluded(\"basicRules2024\")).isTrue();\n\n            assertThat(config.sourceIncluded(\"DMG\")).isFalse();\n            assertThat(config.sourceIncluded(\"PHB\")).isFalse();\n\n            assertThat(config.sourceIncluded(\"XDMG\")).isTrue();\n            assertThat(config.sourceIncluded(\"XPHB\")).isTrue();\n\n            assertThat(config.sourceIncluded(\"MM\")).isFalse();\n            assertThat(config.sourceIncluded(\"MPMM\")).isTrue();\n            assertThat(config.sourceIncluded(\"SCAG\")).isFalse();\n            assertThat(config.sourceIncluded(\"TCE\")).isFalse();\n            assertThat(config.sourceIncluded(\"XGE\")).isFalse();\n\n            assertThat(config.sourceIncluded(\"PaBTSO\")).isTrue();\n            assertThat(config.sourceIncluded(\"dsotdq\")).isTrue();\n            assertThat(config.sourceIncluded(\"OotA\")).isFalse();\n            assertThat(config.sourceIncluded(\"LMOP\")).isFalse();\n\n            commonTests.assert_MISSING(\"action|attack|phb\");\n            commonTests.assert_Present(\"action|attack|xphb\");\n            commonTests.assert_MISSING(\"action|cast a spell|phb\");\n            commonTests.assert_MISSING(\"action|disengage|phb\");\n            commonTests.assert_Present(\"action|disengage|xphb\");\n\n            commonTests.assert_MISSING(\"feat|alert|phb\");\n            commonTests.assert_Present(\"feat|alert|xphb\");\n            commonTests.assert_Present(\"feat|dueling|xphb\");\n            commonTests.assert_MISSING(\"feat|grappler|phb\");\n            commonTests.assert_Present(\"feat|grappler|xphb\");\n            commonTests.assert_MISSING(\"feat|mobile|phb\");\n            commonTests.assert_MISSING(\"feat|moderately armored|phb\");\n            commonTests.assert_Present(\"feat|moderately armored|xphb\");\n\n            commonTests.assert_MISSING(\"variantrule|facing|dmg\");\n            commonTests.assert_MISSING(\"variantrule|falling|xge\");\n            commonTests.assert_MISSING(\"variantrule|familiars|mm\");\n            commonTests.assert_MISSING(\"variantrule|simultaneous effects|xge\");\n            commonTests.assert_Present(\"variantrule|simultaneous effects|xphb\");\n\n            commonTests.assert_MISSING(\"background|sage|phb\");\n            commonTests.assert_Present(\"background|sage|xphb\");\n\n            commonTests.assert_MISSING(\"condition|blinded|phb\");\n            commonTests.assert_Present(\"condition|blinded|xphb\");\n\n            commonTests.assert_MISSING(\"deity|auril|faerûnian|frhof\");\n            commonTests.assert_MISSING(\"deity|auril|faerûnian|scag\");\n            commonTests.assert_MISSING(\"deity|auril|forgotten realms|phb\");\n            commonTests.assert_Present(\"deity|chemosh|dragonlance|dsotdq\");\n            commonTests.assert_MISSING(\"deity|chemosh|dragonlance|phb\");\n            commonTests.assert_MISSING(\"deity|ehlonna|greyhawk|phb\");\n            commonTests.assert_Present(\"deity|ehlonna|greyhawk|xdmg\");\n            commonTests.assert_MISSING(\"deity|gruumsh|dawn war|dmg\");\n            commonTests.assert_MISSING(\"deity|gruumsh|exandria|egw\");\n            commonTests.assert_MISSING(\"deity|gruumsh|nonhuman|phb\");\n            commonTests.assert_MISSING(\"deity|gruumsh|orc|scag\");\n            commonTests.assert_MISSING(\"deity|gruumsh|orc|vgm\");\n            commonTests.assert_MISSING(\"deity|the traveler|eberron|erlw\");\n            commonTests.assert_MISSING(\"deity|the traveler|eberron|phb\");\n            commonTests.assert_MISSING(\"deity|the traveler|exandria|egw\");\n\n            commonTests.assert_MISSING(\"disease|cackle fever|dmg\");\n            commonTests.assert_Present(\"disease|cackle fever|xdmg\");\n\n            commonTests.assert_Present(\"hazard|quicksand pit|xdmg\");\n            commonTests.assert_MISSING(\"hazard|quicksand|dmg\");\n            commonTests.assert_MISSING(\"hazard|razorvine|dmg\");\n            commonTests.assert_Present(\"hazard|razorvine|xdmg\");\n\n            commonTests.assert_MISSING(\"itemgroup|arcane focus|phb\");\n            commonTests.assert_Present(\"itemgroup|arcane focus|xphb\");\n            commonTests.assert_MISSING(\"itemgroup|carpet of flying|dmg\");\n            commonTests.assert_Present(\"itemgroup|carpet of flying|xdmg\");\n            commonTests.assert_MISSING(\"itemgroup|ioun stone|dmg\");\n            commonTests.assert_MISSING(\"itemgroup|ioun stone|llk\");\n            commonTests.assert_Present(\"itemgroup|ioun stone|xdmg\");\n            commonTests.assert_MISSING(\"itemgroup|musical instrument|phb\");\n            commonTests.assert_MISSING(\"itemgroup|musical instrument|scag\");\n            commonTests.assert_Present(\"itemgroup|musical instrument|xphb\");\n            commonTests.assert_MISSING(\"itemgroup|spell scroll|dmg\");\n            commonTests.assert_Present(\"itemgroup|spell scroll|xdmg\");\n\n            commonTests.assert_MISSING(\"itemproperty|2h|phb\");\n            commonTests.assert_Present(\"itemproperty|2h|xphb\");\n            commonTests.assert_MISSING(\"itemproperty|bf|dmg\");\n            commonTests.assert_Present(\"itemproperty|bf|xdmg\");\n            commonTests.assert_MISSING(\"itemproperty|s|phb\");\n\n            commonTests.assert_MISSING(\"itemtype|$c|phb\");\n            commonTests.assert_Present(\"itemtype|$c|xphb\");\n            commonTests.assert_MISSING(\"itemtype|$g|dmg\");\n            commonTests.assert_Present(\"itemtype|$g|xdmg\");\n            commonTests.assert_MISSING(\"itemtype|sc|dmg\");\n            commonTests.assert_Present(\"itemtype|sc|xphb\");\n            commonTests.assert_MISSING(\"itemtypeadditionalentries|gs|phb|xge\");\n\n            commonTests.assert_MISSING(\"item|+1 rod of the pact keeper|dmg\");\n            commonTests.assert_Present(\"item|+1 rod of the pact keeper|xdmg\");\n            commonTests.assert_Present(\"item|+2 wraps of unarmed power|xdmg\");\n            commonTests.assert_MISSING(\"item|+2 wraps of unarmed prowess|bmt\");\n            commonTests.assert_MISSING(\"item|acid (vial)|phb\");\n            commonTests.assert_MISSING(\"item|acid absorbing tattoo|tce\");\n            commonTests.assert_Present(\"item|acid|xphb\");\n            commonTests.assert_MISSING(\"item|alchemist's doom|scc\");\n            commonTests.assert_MISSING(\"item|alchemist's fire (flask)|phb\");\n            commonTests.assert_Present(\"item|alchemist's fire|xphb\");\n            commonTests.assert_MISSING(\"item|alchemist's supplies|phb\");\n            commonTests.assert_Present(\"item|alchemist's supplies|xphb\");\n            commonTests.assert_MISSING(\"item|amulet of health|dmg\");\n            commonTests.assert_Present(\"item|amulet of health|xdmg\");\n            commonTests.assert_MISSING(\"item|amulet of proof against detection and location|dmg\");\n            commonTests.assert_Present(\"item|amulet of proof against detection and location|xdmg\");\n            commonTests.assert_MISSING(\"item|armor of invulnerability|dmg\");\n            commonTests.assert_Present(\"item|armor of invulnerability|xdmg\");\n            commonTests.assert_MISSING(\"item|automatic pistol|dmg\");\n            commonTests.assert_MISSING(\"item|automatic rifle|dmg\");\n            commonTests.assert_Present(\"item|automatic rifle|xdmg\");\n            commonTests.assert_MISSING(\"item|ball bearings (bag of 1,000)|phb\");\n            commonTests.assert_Present(\"item|ball bearings|xphb\");\n            commonTests.assert_MISSING(\"item|ball bearing|phb\");\n            commonTests.assert_MISSING(\"item|chain (10 feet)|phb\");\n            commonTests.assert_Present(\"item|chain|xphb\");\n\n            commonTests.assert_Present(\"monster|abjurer wizard|mpmm\");\n            commonTests.assert_MISSING(\"monster|abjurer|vgm\");\n            commonTests.assert_Present(\"monster|alkilith|mpmm\");\n            commonTests.assert_MISSING(\"monster|alkilith|mtf\");\n            commonTests.assert_MISSING(\"monster|animated object (huge)|phb\");\n            commonTests.assert_MISSING(\"monster|ape|mm\");\n            commonTests.assert_Present(\"monster|ape|xmm\");\n            commonTests.assert_MISSING(\"monster|ash zombie|lmop\");\n            commonTests.assert_Present(\"monster|ash zombie|pabtso\");\n            commonTests.assert_MISSING(\"monster|awakened shrub|mm\");\n            commonTests.assert_Present(\"monster|awakened shrub|xmm\");\n            commonTests.assert_MISSING(\"monster|beast of the land|tce\");\n            commonTests.assert_Present(\"monster|beast of the land|xphb\");\n            commonTests.assert_MISSING(\"monster|bestial spirit (air)|tce\");\n            commonTests.assert_Present(\"monster|bestial spirit (air)|xphb\");\n            commonTests.assert_MISSING(\"monster|cat|mm\");\n            commonTests.assert_Present(\"monster|cat|xmm\");\n            commonTests.assert_Present(\"monster|derro savant|mpmm\");\n            commonTests.assert_MISSING(\"monster|derro savant|mtf\");\n            commonTests.assert_MISSING(\"monster|derro savant|oota\");\n            commonTests.assert_Present(\"monster|sibriex|mpmm\");\n            commonTests.assert_MISSING(\"monster|sibriex|mtf\");\n\n            commonTests.assert_MISSING(\"object|trebuchet|dmg\");\n            commonTests.assert_Present(\"object|trebuchet|xdmg\");\n\n            commonTests.assert_MISSING(\"optfeature|ambush|tce\");\n            commonTests.assert_Present(\"optfeature|ambush|xphb\");\n            commonTests.assert_MISSING(\"optfeature|investment of the chain master|tce\");\n            commonTests.assert_Present(\"optfeature|investment of the chain master|xphb\");\n\n            commonTests.assert_MISSING(\"reward|blessing of weapon enhancement|dmg\");\n            commonTests.assert_Present(\"reward|blessing of weapon enhancement|xdmg\");\n            commonTests.assert_MISSING(\"reward|blessing of wound closure|dmg\");\n            commonTests.assert_Present(\"reward|blessing of wound closure|xdmg\");\n            commonTests.assert_MISSING(\"reward|boon of combat prowess|dmg\");\n            commonTests.assert_MISSING(\"reward|boon of dimensional travel|dmg\");\n            commonTests.assert_MISSING(\"reward|boon of fate|dmg\");\n            commonTests.assert_MISSING(\"reward|boon of fortitude|dmg\");\n            commonTests.assert_MISSING(\"reward|boon of high magic|dmg\");\n\n            commonTests.assert_MISSING(\"sense|blindsight|phb\");\n            commonTests.assert_Present(\"sense|blindsight|xphb\");\n\n            commonTests.assert_MISSING(\"skill|athletics|phb\");\n            commonTests.assert_Present(\"skill|athletics|xphb\");\n\n            commonTests.assert_MISSING(\"spell|acid splash|phb\");\n            commonTests.assert_Present(\"spell|acid splash|xphb\");\n            commonTests.assert_MISSING(\"spell|aganazzar's scorcher|xge\");\n            commonTests.assert_MISSING(\"spell|blade barrier|phb\");\n            commonTests.assert_Present(\"spell|blade barrier|xphb\");\n            commonTests.assert_MISSING(\"spell|feeblemind|phb\");\n            commonTests.assert_MISSING(\"spell|illusory dragon|xge\");\n            commonTests.assert_MISSING(\"spell|illusory script|phb\");\n            commonTests.assert_Present(\"spell|illusory script|xphb\");\n            commonTests.assert_MISSING(\"spell|wrath of nature|xge\");\n\n            commonTests.assert_MISSING(\"status|surprised|phb\");\n            commonTests.assert_Present(\"status|surprised|xphb\");\n\n            commonTests.assert_MISSING(\"trap|collapsing roof|dmg\");\n            commonTests.assert_Present(\"trap|collapsing roof|xdmg\");\n            commonTests.assert_MISSING(\"trap|falling net|dmg\");\n            commonTests.assert_Present(\"trap|falling net|xdmg\");\n            commonTests.assert_MISSING(\"trap|pits|dmg\");\n            commonTests.assert_MISSING(\"trap|poison darts|dmg\");\n            commonTests.assert_Present(\"trap|poisoned darts|xdmg\");\n            commonTests.assert_MISSING(\"trap|poison needle trap|xge\");\n            commonTests.assert_MISSING(\"trap|poison needle|dmg\");\n            commonTests.assert_MISSING(\"trap|rolling sphere|dmg\");\n            commonTests.assert_Present(\"trap|rolling stone|xdmg\");\n\n            commonTests.assert_MISSING(\"vehicle|apparatus of kwalish|dmg\");\n            commonTests.assert_Present(\"vehicle|apparatus of kwalish|xdmg\");\n\n            // Classes, subclasses, class features, and subclass features\n\n            commonTests.assert_MISSING(\"classtype|artificer|efa\");\n            commonTests.assert_MISSING(\"classtype|artificer|tce\");\n\n            // \"Path of Wild Magic|Barbarian||Wild Magic|TCE|3\",\n            // \"Bolstering Magic|Barbarian||Wild Magic|TCE|6\",\n            // \"Unstable Backlash|Barbarian||Wild Magic|TCE|10\",\n            // \"Controlled Surge|Barbarian||Wild Magic|TCE|14\",\n\n            commonTests.assert_MISSING(\"classtype|barbarian|phb\");\n            commonTests.assert_Present(\"classtype|barbarian|xphb\");\n\n            commonTests.assert_MISSING(\"subclass|path of wild magic|barbarian|phb|tce\");\n            commonTests.assert_MISSING(\"subclass|path of wild magic|barbarian|xphb|tce\");\n\n            commonTests.assert_MISSING(\"subclassfeature|bolstering magic|barbarian|phb|wild magic|tce|6|tce\");\n            commonTests.assert_MISSING(\"subclassfeature|controlled surge|barbarian|phb|wild magic|tce|14|tce\");\n            commonTests.assert_MISSING(\"subclassfeature|magic awareness|barbarian|phb|wild magic|tce|3|tce\");\n            commonTests.assert_MISSING(\"subclassfeature|path of wild magic|barbarian|phb|wild magic|tce|3|tce\");\n            commonTests.assert_MISSING(\"subclassfeature|unstable backlash|barbarian|phb|wild magic|tce|10|tce\");\n            commonTests.assert_MISSING(\"subclassfeature|wild surge|barbarian|phb|wild magic|tce|3|tce\");\n\n            // \"Thief|Rogue||Thief||3\",\n            // \"Supreme Sneak|Rogue||Thief||9\",\n            // \"Use Magic Device|Rogue||Thief||13\",\n            // \"Thief's Reflexes|Rogue||Thief||17\"\n\n            commonTests.assert_MISSING(\"classtype|rogue|phb\");\n            commonTests.assert_Present(\"classtype|rogue|xphb\");\n\n            commonTests.assert_MISSING(\"subclass|thief|rogue|phb|phb\");\n            commonTests.assert_MISSING(\"subclass|thief|rogue|xphb|phb\");\n            commonTests.assert_Present(\"subclass|thief|rogue|xphb|xphb\");\n\n            commonTests.assert_MISSING(\"subclassfeature|thief|rogue|phb|thief|phb|3|phb\");\n            commonTests.assert_Present(\"subclassfeature|thief|rogue|xphb|thief|xphb|3|xphb\");\n            commonTests.assert_MISSING(\"subclassfeature|supreme sneak|rogue|phb|thief|phb|9|phb\");\n            commonTests.assert_Present(\"subclassfeature|supreme sneak|rogue|xphb|thief|xphb|9|xphb\");\n            commonTests.assert_MISSING(\"subclassfeature|use magic device|rogue|phb|thief|phb|13|phb\");\n            commonTests.assert_Present(\"subclassfeature|use magic device|rogue|xphb|thief|xphb|13|xphb\");\n            commonTests.assert_MISSING(\"subclassfeature|thief's reflexes|rogue|phb|thief|phb|17|phb\");\n            commonTests.assert_Present(\"subclassfeature|thief's reflexes|rogue|xphb|thief|xphb|17|xphb\");\n\n            // Races and subraces\n\n            commonTests.assert_MISSING(\"race|bugbear|erlw\");\n            commonTests.assert_Present(\"race|bugbear|mpmm\");\n            commonTests.assert_MISSING(\"race|bugbear|vgm\");\n            commonTests.assert_MISSING(\"race|human|phb\");\n            commonTests.assert_Present(\"race|human|xphb\");\n            commonTests.assert_MISSING(\"race|tiefling|phb\");\n            commonTests.assert_Present(\"race|tiefling|xphb\");\n            commonTests.assert_MISSING(\"race|warforged|efa\");\n            commonTests.assert_MISSING(\"race|warforged|erlw\");\n            commonTests.assert_MISSING(\"race|yuan-ti pureblood|vgm\");\n            commonTests.assert_Present(\"race|yuan-ti|mpmm\");\n\n            commonTests.assert_MISSING(\"subrace|genasi (air)|genasi|eepc|eepc\");\n            commonTests.assert_Present(\"subrace|genasi (air)|genasi|mpmm|mpmm\");\n            commonTests.assert_MISSING(\"subrace|human|human|phb|phb\");\n            commonTests.assert_MISSING(\"subrace|tiefling (zariel)|tiefling|phb|mtf\");\n            commonTests.assert_MISSING(\"subrace|tiefling|tiefling|phb|phb\");\n            commonTests.assert_MISSING(\"subrace|vampire (ixalan)|vampire|psz|psx\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/java/dev/ebullient/convert/tools/dnd5e/FilterSubsetMixedTest.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\nimport dev.ebullient.convert.TestUtils;\nimport dev.ebullient.convert.tools.dnd5e.CommonDataTests.TestInput;\nimport io.quarkus.test.junit.QuarkusTest;\n\n@QuarkusTest\n\npublic class FilterSubsetMixedTest {\n    static CommonDataTests commonTests;\n    static final TestInput testInput = TestInput.subsetMixed;\n    static final Path outputPath = TestUtils.OUTPUT_5E_DATA.resolve(testInput.name());\n\n    @BeforeAll\n    public static void setupDir() throws Exception {\n        outputPath.toFile().mkdirs();\n        String config = \"\"\"\n                {\n                    \"sources\": {\n                        \"book\": [\n                            \"XDMG\",\n                            \"XPHB\"\n                        ],\n                        \"adventure\": [\n                            \"PaBTSO\",\n                            \"OotA\"\n                        ],\n                        \"reference\": [\n                            \"MM\",\n                            \"MPMM\",\n                            \"SCAG\",\n                            \"TCE\",\n                            \"XGE\"\n                        ],\n                        \"homebrew\": [\n                            \"sources/5e-homebrew/collection/Keith Baker; Exploring Eberron.json\",\n                            \"sources/5e-homebrew/class/Matthew Mercer; Blood Hunter (2022).json\"\n                        ]\n                    },\n                    \"include\": [\n                      \"subclass|death domain|cleric|phb|dmg\"\n                    ],\n                    \"images\": {\n                        \"copyInternal\": false\n                    },\n                    \"useDiceRoller\" : false\n                }\n                \"\"\".stripIndent();\n        commonTests = new CommonDataTests(testInput, config, TestUtils.PATH_5E_TOOLS_DATA);\n    }\n\n    @AfterAll\n    public static void done() throws IOException {\n        commonTests.afterAll(outputPath);\n    }\n\n    @AfterEach\n    public void cleanup() throws Exception {\n        commonTests.afterEach();\n    }\n\n    @Test\n    public void testKeyIndex() throws Exception {\n        commonTests.testKeyIndex(outputPath);\n\n        if (commonTests.dataPresent) {\n            var config = commonTests.config;\n\n            assertThat(config.sourceIncluded(\"srd\")).isFalse();\n            assertThat(config.sourceIncluded(\"basicrules\")).isFalse();\n            assertThat(config.sourceIncluded(\"srd52\")).isFalse();\n            assertThat(config.sourceIncluded(\"basicRules2024\")).isFalse();\n\n            assertThat(config.sourceIncluded(\"DMG\")).isFalse();\n            assertThat(config.sourceIncluded(\"PHB\")).isFalse();\n\n            assertThat(config.sourceIncluded(\"XDMG\")).isTrue();\n            assertThat(config.sourceIncluded(\"XPHB\")).isTrue();\n\n            assertThat(config.sourceIncluded(\"MM\")).isTrue();\n            assertThat(config.sourceIncluded(\"MPMM\")).isTrue();\n            assertThat(config.sourceIncluded(\"SCAG\")).isTrue();\n            assertThat(config.sourceIncluded(\"TCE\")).isTrue();\n            assertThat(config.sourceIncluded(\"XGE\")).isTrue();\n\n            assertThat(config.sourceIncluded(\"PaBTSO\")).isTrue();\n            assertThat(config.sourceIncluded(\"OotA\")).isTrue();\n            assertThat(config.sourceIncluded(\"LMOP\")).isFalse();\n\n            commonTests.assert_MISSING(\"action|attack|phb\");\n            commonTests.assert_Present(\"action|attack|xphb\");\n            commonTests.assert_MISSING(\"action|cast a spell|phb\");\n            commonTests.assert_MISSING(\"action|disengage|phb\");\n            commonTests.assert_Present(\"action|disengage|xphb\");\n\n            commonTests.assert_MISSING(\"feat|alert|phb\");\n            commonTests.assert_Present(\"feat|alert|xphb\");\n            commonTests.assert_Present(\"feat|dueling|xphb\");\n            commonTests.assert_MISSING(\"feat|grappler|phb\");\n            commonTests.assert_Present(\"feat|grappler|xphb\");\n            commonTests.assert_MISSING(\"feat|mobile|phb\");\n            commonTests.assert_MISSING(\"feat|moderately armored|phb\");\n            commonTests.assert_Present(\"feat|moderately armored|xphb\");\n\n            commonTests.assert_MISSING(\"variantrule|facing|dmg\");\n            commonTests.assert_MISSING(\"variantrule|falling|xge\");\n            commonTests.assert_Present(\"variantrule|familiars|mm\");\n            commonTests.assert_MISSING(\"variantrule|simultaneous effects|xge\");\n            commonTests.assert_Present(\"variantrule|simultaneous effects|xphb\");\n\n            commonTests.assert_MISSING(\"background|sage|phb\");\n            commonTests.assert_Present(\"background|sage|xphb\");\n            commonTests.assert_MISSING(\"background|baldur's gate acolyte|bgdia\");\n\n            commonTests.assert_MISSING(\"condition|blinded|phb\");\n            commonTests.assert_Present(\"condition|blinded|xphb\");\n\n            commonTests.assert_MISSING(\"deity|auril|faerûnian|frhof\");\n            commonTests.assert_Present(\"deity|auril|faerûnian|scag\");\n            commonTests.assert_MISSING(\"deity|auril|forgotten realms|phb\");\n            commonTests.assert_MISSING(\"deity|chemosh|dragonlance|dsotdq\");\n            commonTests.assert_MISSING(\"deity|chemosh|dragonlance|phb\");\n            commonTests.assert_MISSING(\"deity|ehlonna|greyhawk|phb\");\n            commonTests.assert_Present(\"deity|ehlonna|greyhawk|xdmg\");\n            commonTests.assert_MISSING(\"deity|gruumsh|dawn war|dmg\");\n            commonTests.assert_MISSING(\"deity|gruumsh|exandria|egw\");\n            commonTests.assert_MISSING(\"deity|gruumsh|nonhuman|phb\");\n            commonTests.assert_Present(\"deity|gruumsh|orc|scag\");\n            commonTests.assert_MISSING(\"deity|gruumsh|orc|vgm\");\n            commonTests.assert_MISSING(\"deity|the traveler|eberron|erlw\");\n            commonTests.assert_MISSING(\"deity|the traveler|eberron|phb\");\n            commonTests.assert_MISSING(\"deity|the traveler|exandria|egw\");\n\n            commonTests.assert_MISSING(\"disease|cackle fever|dmg\");\n            commonTests.assert_Present(\"disease|cackle fever|xdmg\");\n\n            commonTests.assert_Present(\"hazard|quicksand pit|xdmg\");\n            commonTests.assert_MISSING(\"hazard|quicksand|dmg\");\n            commonTests.assert_MISSING(\"hazard|razorvine|dmg\");\n            commonTests.assert_Present(\"hazard|razorvine|xdmg\");\n\n            commonTests.assert_MISSING(\"itemgroup|arcane focus|phb\");\n            commonTests.assert_Present(\"itemgroup|arcane focus|xphb\");\n            commonTests.assert_MISSING(\"itemgroup|carpet of flying|dmg\");\n            commonTests.assert_Present(\"itemgroup|carpet of flying|xdmg\");\n            commonTests.assert_MISSING(\"itemgroup|ioun stone|dmg\");\n            commonTests.assert_MISSING(\"itemgroup|ioun stone|llk\");\n            commonTests.assert_Present(\"itemgroup|ioun stone|xdmg\");\n            commonTests.assert_MISSING(\"itemgroup|musical instrument|phb\");\n            commonTests.assert_MISSING(\"itemgroup|musical instrument|scag\");\n            commonTests.assert_Present(\"itemgroup|musical instrument|xphb\");\n            commonTests.assert_MISSING(\"itemgroup|spell scroll|dmg\");\n            commonTests.assert_Present(\"itemgroup|spell scroll|xdmg\");\n\n            commonTests.assert_MISSING(\"itemproperty|2h|phb\");\n            commonTests.assert_Present(\"itemproperty|2h|xphb\");\n            commonTests.assert_MISSING(\"itemproperty|bf|dmg\");\n            commonTests.assert_Present(\"itemproperty|bf|xdmg\");\n            commonTests.assert_MISSING(\"itemproperty|s|phb\");\n\n            commonTests.assert_MISSING(\"itemtype|$c|phb\");\n            commonTests.assert_Present(\"itemtype|$c|xphb\");\n            commonTests.assert_MISSING(\"itemtype|$g|dmg\");\n            commonTests.assert_Present(\"itemtype|$g|xdmg\");\n            commonTests.assert_MISSING(\"itemtype|sc|dmg\");\n            commonTests.assert_Present(\"itemtype|sc|xphb\");\n            commonTests.assert_Present(\"itemtypeadditionalentries|gs|phb|xge\");\n\n            commonTests.assert_MISSING(\"item|+1 rod of the pact keeper|dmg\");\n            commonTests.assert_Present(\"item|+1 rod of the pact keeper|xdmg\");\n            commonTests.assert_Present(\"item|+2 wraps of unarmed power|xdmg\");\n            commonTests.assert_MISSING(\"item|+2 wraps of unarmed prowess|bmt\");\n            commonTests.assert_MISSING(\"item|acid (vial)|phb\");\n            commonTests.assert_Present(\"item|acid absorbing tattoo|tce\");\n            commonTests.assert_Present(\"item|acid|xphb\");\n            commonTests.assert_MISSING(\"item|alchemist's doom|scc\");\n            commonTests.assert_MISSING(\"item|alchemist's fire (flask)|phb\");\n            commonTests.assert_Present(\"item|alchemist's fire|xphb\");\n            commonTests.assert_MISSING(\"item|alchemist's supplies|phb\");\n            commonTests.assert_Present(\"item|alchemist's supplies|xphb\");\n            commonTests.assert_MISSING(\"item|amulet of health|dmg\");\n            commonTests.assert_Present(\"item|amulet of health|xdmg\");\n            commonTests.assert_MISSING(\"item|amulet of proof against detection and location|dmg\");\n            commonTests.assert_Present(\"item|amulet of proof against detection and location|xdmg\");\n            commonTests.assert_MISSING(\"item|armor of invulnerability|dmg\");\n            commonTests.assert_Present(\"item|armor of invulnerability|xdmg\");\n            commonTests.assert_MISSING(\"item|automatic pistol|dmg\");\n            commonTests.assert_MISSING(\"item|automatic rifle|dmg\");\n            commonTests.assert_Present(\"item|automatic rifle|xdmg\");\n            commonTests.assert_MISSING(\"item|ball bearings (bag of 1,000)|phb\");\n            commonTests.assert_Present(\"item|ball bearings|xphb\");\n            commonTests.assert_MISSING(\"item|ball bearing|phb\");\n            commonTests.assert_MISSING(\"item|chain (10 feet)|phb\");\n            commonTests.assert_Present(\"item|chain|xphb\");\n\n            commonTests.assert_Present(\"monster|abjurer wizard|mpmm\");\n            commonTests.assert_MISSING(\"monster|abjurer|vgm\");\n            commonTests.assert_Present(\"monster|alkilith|mpmm\");\n            commonTests.assert_MISSING(\"monster|alkilith|mtf\");\n            commonTests.assert_MISSING(\"monster|animated object (huge)|phb\");\n            commonTests.assert_MISSING(\"monster|ape|mm\");\n            commonTests.assert_Present(\"monster|ape|xmm\");\n            commonTests.assert_MISSING(\"monster|ash zombie|lmop\");\n            commonTests.assert_Present(\"monster|ash zombie|pabtso\");\n            commonTests.assert_Present(\"monster|awakened shrub|mm\");\n            commonTests.assert_MISSING(\"monster|awakened shrub|xmm\");\n            commonTests.assert_MISSING(\"monster|beast of the land|tce\");\n            commonTests.assert_Present(\"monster|beast of the land|xphb\");\n            commonTests.assert_MISSING(\"monster|bestial spirit (air)|tce\");\n            commonTests.assert_Present(\"monster|bestial spirit (air)|xphb\");\n            commonTests.assert_MISSING(\"monster|cat|mm\");\n            commonTests.assert_Present(\"monster|cat|xmm\");\n            commonTests.assert_Present(\"monster|derro savant|mpmm\"); // supersedes both mtf & oota\n            commonTests.assert_MISSING(\"monster|derro savant|mtf\");\n            commonTests.assert_MISSING(\"monster|derro savant|oota\");\n            commonTests.assert_Present(\"monster|sibriex|mpmm\");\n            commonTests.assert_MISSING(\"monster|sibriex|mtf\");\n\n            commonTests.assert_MISSING(\"object|trebuchet|dmg\");\n            commonTests.assert_Present(\"object|trebuchet|xdmg\");\n\n            commonTests.assert_MISSING(\"optfeature|ambush|tce\");\n            commonTests.assert_Present(\"optfeature|ambush|xphb\");\n            commonTests.assert_MISSING(\"optfeature|investment of the chain master|tce\");\n            commonTests.assert_Present(\"optfeature|investment of the chain master|xphb\");\n\n            commonTests.assert_MISSING(\"reward|blessing of weapon enhancement|dmg\");\n            commonTests.assert_Present(\"reward|blessing of weapon enhancement|xdmg\");\n            commonTests.assert_MISSING(\"reward|blessing of wound closure|dmg\");\n            commonTests.assert_Present(\"reward|blessing of wound closure|xdmg\");\n            commonTests.assert_MISSING(\"reward|boon of combat prowess|dmg\");\n            commonTests.assert_MISSING(\"reward|boon of dimensional travel|dmg\");\n            commonTests.assert_MISSING(\"reward|boon of fate|dmg\");\n            commonTests.assert_MISSING(\"reward|boon of fortitude|dmg\");\n            commonTests.assert_MISSING(\"reward|boon of high magic|dmg\");\n\n            commonTests.assert_MISSING(\"sense|blindsight|phb\");\n            commonTests.assert_Present(\"sense|blindsight|xphb\");\n\n            commonTests.assert_MISSING(\"skill|athletics|phb\");\n            commonTests.assert_Present(\"skill|athletics|xphb\");\n\n            commonTests.assert_MISSING(\"spell|acid splash|phb\");\n            commonTests.assert_Present(\"spell|acid splash|xphb\");\n            commonTests.assert_Present(\"spell|aganazzar's scorcher|xge\");\n            commonTests.assert_MISSING(\"spell|blade barrier|phb\");\n            commonTests.assert_Present(\"spell|blade barrier|xphb\");\n            commonTests.assert_MISSING(\"spell|feeblemind|phb\");\n            commonTests.assert_Present(\"spell|illusory dragon|xge\");\n            commonTests.assert_MISSING(\"spell|illusory script|phb\");\n            commonTests.assert_Present(\"spell|illusory script|xphb\");\n            commonTests.assert_Present(\"spell|wrath of nature|xge\");\n\n            commonTests.assert_MISSING(\"status|surprised|phb\");\n            commonTests.assert_Present(\"status|surprised|xphb\");\n\n            commonTests.assert_MISSING(\"trap|collapsing roof|dmg\");\n            commonTests.assert_Present(\"trap|collapsing roof|xdmg\");\n            commonTests.assert_MISSING(\"trap|falling net|dmg\");\n            commonTests.assert_Present(\"trap|falling net|xdmg\");\n            commonTests.assert_MISSING(\"trap|pits|dmg\");\n            commonTests.assert_MISSING(\"trap|poison darts|dmg\");\n            commonTests.assert_Present(\"trap|poisoned darts|xdmg\");\n            commonTests.assert_Present(\"trap|poison needle trap|xge\");\n            commonTests.assert_MISSING(\"trap|poison needle|dmg\");\n            commonTests.assert_MISSING(\"trap|rolling sphere|dmg\");\n            commonTests.assert_Present(\"trap|rolling stone|xdmg\");\n\n            commonTests.assert_MISSING(\"vehicle|apparatus of kwalish|dmg\");\n            commonTests.assert_Present(\"vehicle|apparatus of kwalish|xdmg\");\n\n            // Classes, subclasses, class features, and subclass features\n\n            commonTests.assert_MISSING(\"classtype|artificer|efa\");\n            commonTests.assert_Present(\"classtype|artificer|tce\");\n\n            // \"Path of Wild Magic|Barbarian||Wild Magic|TCE|3\",\n            // \"Bolstering Magic|Barbarian||Wild Magic|TCE|6\",\n            // \"Unstable Backlash|Barbarian||Wild Magic|TCE|10\",\n            // \"Controlled Surge|Barbarian||Wild Magic|TCE|14\",\n\n            commonTests.assert_MISSING(\"classtype|barbarian|phb\");\n            commonTests.assert_Present(\"classtype|barbarian|xphb\");\n\n            commonTests.assert_MISSING(\"subclass|path of wild magic|barbarian|phb|tce\");\n            commonTests.assert_Present(\"subclass|path of wild magic|barbarian|xphb|tce\");\n\n            commonTests.assert_Present(\"subclassfeature|bolstering magic|barbarian|phb|wild magic|tce|6|tce\");\n            commonTests.assert_Present(\"subclassfeature|controlled surge|barbarian|phb|wild magic|tce|14|tce\");\n            commonTests.assert_Present(\"subclassfeature|magic awareness|barbarian|phb|wild magic|tce|3|tce\");\n            commonTests.assert_Present(\"subclassfeature|path of wild magic|barbarian|phb|wild magic|tce|3|tce\");\n            commonTests.assert_Present(\"subclassfeature|unstable backlash|barbarian|phb|wild magic|tce|10|tce\");\n            commonTests.assert_Present(\"subclassfeature|wild surge|barbarian|phb|wild magic|tce|3|tce\");\n\n            // \"Thief|Rogue||Thief||3\",\n            // \"Supreme Sneak|Rogue||Thief||9\",\n            // \"Use Magic Device|Rogue||Thief||13\",\n            // \"Thief's Reflexes|Rogue||Thief||17\"\n\n            commonTests.assert_MISSING(\"classtype|rogue|phb\");\n            commonTests.assert_Present(\"classtype|rogue|xphb\");\n\n            commonTests.assert_MISSING(\"subclass|thief|rogue|phb|phb\");\n            commonTests.assert_MISSING(\"subclass|thief|rogue|xphb|phb\");\n            commonTests.assert_Present(\"subclass|thief|rogue|xphb|xphb\");\n\n            commonTests.assert_MISSING(\"subclassfeature|thief|rogue|phb|thief|phb|3|phb\");\n            commonTests.assert_Present(\"subclassfeature|thief|rogue|xphb|thief|xphb|3|xphb\");\n            commonTests.assert_MISSING(\"subclassfeature|supreme sneak|rogue|phb|thief|phb|9|phb\");\n            commonTests.assert_Present(\"subclassfeature|supreme sneak|rogue|xphb|thief|xphb|9|xphb\");\n            commonTests.assert_MISSING(\"subclassfeature|use magic device|rogue|phb|thief|phb|13|phb\");\n            commonTests.assert_Present(\"subclassfeature|use magic device|rogue|xphb|thief|xphb|13|xphb\");\n            commonTests.assert_MISSING(\"subclassfeature|thief's reflexes|rogue|phb|thief|phb|17|phb\");\n            commonTests.assert_Present(\"subclassfeature|thief's reflexes|rogue|xphb|thief|xphb|17|xphb\");\n\n            commonTests.assert_MISSING(\"race|bugbear|erlw\");\n            commonTests.assert_Present(\"race|bugbear|mpmm\");\n            commonTests.assert_MISSING(\"race|bugbear|vgm\");\n            commonTests.assert_MISSING(\"race|human|phb\");\n            commonTests.assert_Present(\"race|human|xphb\");\n            commonTests.assert_MISSING(\"race|tiefling|phb\");\n            commonTests.assert_Present(\"race|tiefling|xphb\");\n            commonTests.assert_MISSING(\"race|warforged|efa\");\n            commonTests.assert_MISSING(\"race|warforged|erlw\");\n            commonTests.assert_MISSING(\"race|yuan-ti pureblood|vgm\");\n            commonTests.assert_Present(\"race|yuan-ti|mpmm\");\n\n            commonTests.assert_MISSING(\"subrace|genasi (air)|genasi|eepc|eepc\");\n            commonTests.assert_Present(\"subrace|genasi (air)|genasi|mpmm|mpmm\");\n            commonTests.assert_MISSING(\"subrace|human|human|phb|phb\");\n            commonTests.assert_MISSING(\"subrace|tiefling (zariel)|tiefling|phb|mtf\");\n            commonTests.assert_MISSING(\"subrace|tiefling|tiefling|phb|phb\");\n            commonTests.assert_MISSING(\"subrace|vampire (ixalan)|vampire|psz|psx\");\n\n            // specific for this combination of features (homebrew)\n            var dirgeSinger = \"subclass|college of the dirge singer|bard|phb|exploringeberron\";\n            commonTests.assert_Present(dirgeSinger);\n\n            // homebrew subclass should be moved to xphb class version (as xphb is present)\n\n            // the phb version of the subclass should not be present in this configuration\n            var subclasses = commonTests.index.findSubclasses(\"classtype|bard|phb\");\n            assertThat(subclasses).isNotNull();\n            assertThat(subclasses).isEmpty();\n\n            // the xphb version should be present\n            subclasses = commonTests.index.findSubclasses(\"classtype|bard|xphb\");\n            assertThat(subclasses).isNotNull();\n            assertThat(subclasses).isNotEmpty();\n            assertThat(subclasses).contains(dirgeSinger);\n\n            // dirge singer should have features\n            var features = commonTests.index.findClassFeatures(dirgeSinger);\n            assertThat(features).isNotNull();\n            assertThat(features).isNotEmpty();\n        }\n    }\n\n    @Test\n    public void testClassList() throws IOException {\n        if (!commonTests.dataPresent) {\n            return;\n        }\n\n        commonTests.testClassList(outputPath);\n\n        String filename = linkifier().getSubclassResource(\n                \"college of the dirge singer\", \"bard\", \"xphb\", \"exploringeberron\")\n                + \".md\";\n\n        Path dirgeSinger = outputPath\n                .resolve(commonTests.index.compendiumFilePath())\n                .resolve(linkifier().getRelativePath(Tools5eIndexType.classtype))\n                .resolve(filename);\n        assertThat(dirgeSinger).exists();\n\n        String content = Files.readString(dirgeSinger);\n        assertThat(content).contains(\"Mixed edition content\");\n    }\n\n    @Test\n    public void testRules() {\n        commonTests.testRules(outputPath);\n    }\n\n    private static Tools5eLinkifier linkifier() {\n        return Tools5eLinkifier.instance();\n    }\n}\n"
  },
  {
    "path": "src/test/java/dev/ebullient/convert/tools/dnd5e/TextReplacementTest.java",
    "content": "package dev.ebullient.convert.tools.dnd5e;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.util.List;\nimport java.util.regex.Matcher;\n\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport dev.ebullient.convert.config.CompendiumConfig;\nimport dev.ebullient.convert.config.CompendiumConfig.Configurator;\nimport dev.ebullient.convert.config.CompendiumConfig.DiceRoller;\nimport dev.ebullient.convert.config.ConfiguratorUtil;\nimport dev.ebullient.convert.io.Tui;\nimport dev.ebullient.convert.tools.JsonSourceCopier;\n\npublic class TextReplacementTest implements JsonSource {\n\n    Tui tui = new Tui();\n    CompendiumConfig config = ConfiguratorUtil.createNewConfig(tui);\n\n    Tools5eIndex index = new Tools5eIndex(config) {\n        @Override\n        public boolean isIncluded(String name) {\n            return true;\n        }\n    };\n\n    @BeforeEach\n    public void before() {\n        Tools5eLinkifier.instance().reset();\n    }\n\n    @Test\n    public void testToHitStr() {\n        String s = \" +<$to_hit__str$> \";\n        Matcher m = JsonSourceCopier.VARIABLE_SUBST_PAT.matcher(s);\n        assertThat(m.find()).isTrue();\n        assertThat(m.group(\"variable\")).isEqualTo(\"to_hit__str\");\n        String[] pieces = m.group(\"variable\").split(\"__\");\n        assertThat(pieces).containsExactly(\"to_hit\", \"str\");\n    }\n\n    @Test\n    public void testDamageAvg() {\n        String s = \"2.5+str\";\n        Matcher m = Tools5eJsonSourceCopier.dmg_avg_subst.matcher(s);\n        assertThat(m.matches()).describedAs(\"damage_avg regex should match \" + s).isTrue();\n        assertThat(m.group(1)).isEqualTo(\"2.5\");\n        assertThat(m.group(2)).isEqualTo(\"+\");\n        assertThat(m.group(3)).isEqualTo(\"str\");\n    }\n\n    @Test\n    public void testPromptString() {\n        // #$prompt_number:default=0,min=0,max=2,title=Enter +2 if the characters provide a bribe or incentive$#\n        // #$prompt_number:min=1,title=Enter the creature's CR!,default=1$#\n\n        String value = \"{@dice d12 + #$prompt_number:default=0,min=0,max=2,title=Enter +2 if the characters provide a bribe or incentive$#}\";\n        String result = this.replacePromptStrings(value);\n        assertThat(result).isEqualTo(\n                \"{@dice d12 + <span title='default=0, max=2, min=0'>[+2 if the characters provide a bribe or incentive]</span>}\");\n\n        value = \"{@dice d20 + #$prompt_number:title=Enter a Modifier$#}\";\n        result = this.replacePromptStrings(value);\n        assertThat(result).isEqualTo(\"{@dice d20 + <span>[Modifier]</span>}\");\n\n        value = \"{@dice 2d4 + #$prompt_number:title=Enter Alert Level$#}\";\n        result = this.replacePromptStrings(value);\n        assertThat(result).isEqualTo(\"{@dice 2d4 + <span>[Alert Level]</span>}\");\n\n        value = \"#$prompt_number:min=1,title=Enter the creature's CR!,default=1$#\";\n        result = this.replacePromptStrings(value);\n        assertThat(result).isEqualTo(\"<span title='default=1, min=1'>[creature's CR]</span>\");\n\n        value = \"{@dice #$prompt_number:min=1,title=Number of crew,default=10$#d4|1d4} gp per crew\";\n        result = this.replacePromptStrings(value);\n        assertThat(result).isEqualTo(\"{@dice <span title='default=10, min=1'>[Number of crew]</span>d4|1d4} gp per crew\");\n\n        value = \"#$prompt_number:min=1,max=5,default=123$#\";\n        result = this.replacePromptStrings(value);\n        assertThat(result).isEqualTo(\"<span title='max=5, min=1'>[123]</span>\");\n\n        value = \"{@damage #$prompt_number:min=1,max=13,title=Enter amount of psi to spend!,default=1$#|1}\";\n        result = this.replacePromptStrings(value);\n        assertThat(result).isEqualTo(\"{@damage <span title='default=1, max=13, min=1'>[amount of psi to spend]</span>|1}\");\n\n        value = \"{@dice 1d20+#$prompt_number:min=1,title=Enter your spell attack bonus$#|+X}\";\n        result = this.replacePromptStrings(value);\n        assertThat(result).isEqualTo(\"{@dice 1d20+<span title='min=1'>[spell attack bonus]</span>|+X}\");\n\n        value = \"{@damage (#$prompt_number:min=1,title=Enter the creature's CR!,default=1$#)d8|A number of  d8s of necrotic damage equal to the creature's challenge rating}\";\n        result = this.replacePromptStrings(value);\n        assertThat(result).isEqualTo(\n                \"{@damage (<span title='default=1, min=1'>[creature's CR]</span>)d8|A number of  d8s of necrotic damage equal to the creature's challenge rating}\");\n    }\n\n    @Test\n    public void testDiceTableHeader() {\n        String value = \"{@dice d12 + #$prompt_number:default=0,min=0,max=2,title=Enter +2 if the characters provide a bribe or incentive$#}\";\n        String result = this.tableHeader(value);\n        assertThat(result).isEqualTo(\n                \"d12 + <span title='default=0, max=2, min=0'>[+2 if the characters provide a bribe or incentive]</span>\");\n\n        value = \"{@dice 1d6;2d6|d6s}\";\n        result = this.tableHeader(value);\n        assertThat(result).isEqualTo(\"1d6;2d6\");\n\n        value = \"{@dice d6;d8}\";\n        result = this.tableHeader(value);\n        assertThat(result).isEqualTo(\"d6;d8\");\n    }\n\n    @Test\n    public void testDiceFormla() {\n        Configurator configurator = new Configurator(tui);\n\n        List<String> example = List.of(\n                \"Spells cast from the spell gem have a save DC of 15 and an attack bonus of {@hit 9}.\",\n                \"It has a Strength of 26 ({@d20 8}) and a Dexterity of 10 ({@d20 0})\",\n                \"{@dice 1d2-2+2d3+5} for regular dice rolls\",\n                \"{@dice 1d6;2d6} for multiple options;\",\n                \"{@dice 1d6 + #$prompt_number:min=1,title=Enter a Number!,default=123$#} for input prompts)\",\n                \"with extended {@dice 1d20+2|display text} and {@dice 1d20+2|display text|rolled by name}\",\n                \"a special 'hit' version which assumes a d20 is to be rolled {@hit +7}\",\n                \"There's also {@damage 1d12+3} and {@d20 -4}\",\n                \"scaledamage: {@scaledamage 2d6;3d6|2-9|1d6}, scaledice: {@scaledice 2d6|1,3,5,7,9|1d6|psi|extra amount}\",\n                \"{@ability str 20}, {@savingThrow str 5}, and {@skillCheck animal_handling 5}\",\n                \"with an Intelligence of {@d20 3|16}, a Wisdom of {@d20 0|10}, and a Charisma of {@d20 4|18}\",\n                \"{@hit 3 plus PB} to hit;  {@h}7 ({@damage 1d6 + 4}) piercing damage plus 7 ({@damage 2d6}) poison damage.\",\n                \"{@d20 2|10|Perception} {@d20 -2|8|Perception}\",\n                \"hit display text: {@hit +3|+3 to hit}\",\n                \"{@atk mw} {@hit 9} to hit, reach 5 ft., one target. {@h}9 ({@damage 1d8 + 5}) piercing damage plus 7 ({@damage 2d6}) necrotic damage.\");\n\n        List<String> disabled = List.of(\n                \"Spells cast from the spell gem have a save DC of 15 and an attack bonus of `+9`.\",\n                \"It has a Strength of 26 (`+8`) and a Dexterity of 10 (`+0`)\",\n                \"`1d2-2+2d3+5` for regular dice rolls\",\n                \"`1d6` or `2d6` for multiple options;\",\n                \"<span title='default=123, min=1'>`1d6 + [Number]`</span> for input prompts)\",\n                \"with extended display text (`+2`) and display text (`+2`)\",\n                \"a special 'hit' version which assumes a d20 is to be rolled `+7`\",\n                \"There's also `1d12+3` and `-4`\",\n                \"scaledamage: `1d6`, scaledice: extra amount (`1d6`)\",\n                \"<span title='Strength'>`20` (`+5`)</span>, <span title='Strength'>`+5`</span>, and [Animal Handling](rules/skills.md#Animal%20Handling) (`+5`)\",\n                \"with an Intelligence of `+3` (`16`), a Wisdom of `+0` (`10`), and a Charisma of `+4` (`18`)\",\n                \"`+3 plus PB` to hit;  *Hit:* 7 (`1d6 + 4`) piercing damage plus 7 (`2d6`) poison damage.\",\n                \"Perception (`+2`) Perception (`-2`)\",\n                \"hit display text: +3 to hit\",\n                \"*Melee Weapon Attack:* `+9` to hit, reach 5 ft., one target. *Hit:* 9 (`1d8 + 5`) piercing damage plus 7 (`2d6`) necrotic damage.\");\n\n        configurator.setUseDiceRoller(DiceRoller.disabled);\n        for (int i = 0; i < example.size(); i++) {\n            String result = this.replaceText(example.get(i));\n            assertThat(result).isEqualTo(disabled.get(i));\n        }\n\n        List<String> enabled = List.of(\n                \"Spells cast from the spell gem have a save DC of 15 and an attack bonus of `dice:1d20+9|noform|noparens|text(+9)`.\",\n                \"It has a Strength of `dice:1d20+8|noform|noparens|text(26)` (`+8`) and a Dexterity of `dice:1d20+0|noform|noparens|text(10)` (`+0`)\",\n                \"`dice:1d2-2+2d3+5|noform|noparens|avg` (`1d2-2+2d3+5`) for regular dice rolls\",\n                \"`dice:1d6|noform|noparens|avg|text(1d6)` or `dice:2d6|noform|noparens|avg|text(2d6)` for multiple options;\",\n                \"<span title='default=123, min=1'>`1d6 + [Number]`</span> for input prompts)\",\n                \"with extended `dice:1d20+2|noform|noparens|avg|text(display text)` (`+2`) and `dice:1d20+2|noform|noparens|avg|text(display text)` (`+2`)\",\n                \"a special 'hit' version which assumes a d20 is to be rolled `dice:1d20+7|noform|noparens|text(+7)`\",\n                \"There's also `dice:1d12+3|noform|noparens|avg` (`1d12+3`) and `dice:1d20-4|noform|noparens|text(-4)`\",\n                \"scaledamage: `dice:1d6|noform|noparens|avg|text(1d6)`, scaledice: `dice:1d6|noform|noparens|avg|text(extra amount)` (`1d6`)\",\n                \"<span title='Strength'>`20` (`dice:d20+5|noform|noparens|text(+5)`)</span>, <span title='Strength'>`dice:d20+5|noform|noparens|text(+5)`</span>, and [Animal Handling](rules/skills.md#Animal%20Handling) (`dice:1d20+5|noform|noparens|text(+5)`)\",\n                \"with an Intelligence of `dice:1d20+3|noform|noparens|text(+3)` (`16`), a Wisdom of `dice:1d20+0|noform|noparens|text(+0)` (`10`), and a Charisma of `dice:1d20+4|noform|noparens|text(+4)` (`18`)\",\n                \"`+3 plus PB` to hit;  *Hit:* `dice:1d6+4|noform|noparens|avg|text(7)` (`1d6 + 4`) piercing damage plus `dice:2d6|noform|noparens|avg|text(7)` (`2d6`) poison damage.\",\n                \"Perception (`dice:1d20+2|noform|noparens|text(+2)`) Perception (`dice:1d20-2|noform|noparens|text(-2)`)\",\n                \"hit display text: +3 to hit\",\n                \"*Melee Weapon Attack:* `dice:1d20+9|noform|noparens|text(+9)` to hit, reach 5 ft., one target. *Hit:* `dice:1d8+5|noform|noparens|avg|text(9)` (`1d8 + 5`) piercing damage plus `dice:2d6|noform|noparens|avg|text(7)` (`2d6`) necrotic damage.\");\n\n        configurator.setUseDiceRoller(DiceRoller.enabled);\n        for (int i = 0; i < example.size(); i++) {\n            String result = this.replaceText(example.get(i));\n            assertThat(result).isEqualTo(enabled.get(i));\n        }\n\n        // With no context change, all should render the same\n        configurator.setUseDiceRoller(DiceRoller.enabledUsingFS);\n        for (int i = 0; i < example.size(); i++) {\n            String result = this.replaceText(example.get(i));\n            assertThat(result).isEqualTo(enabled.get(i));\n        }\n\n        List<String> traits = List.of(\n                \"Spells cast from the spell gem have a save DC of 15 and an attack bonus of +9.\",\n                \"It has a Strength of 26 (+8) and a Dexterity of 10 (+0)\",\n                \"1d2-2+2d3+5 for regular dice rolls\",\n                \"1d6 or 2d6 for multiple options;\",\n                \"<span title='default=123, min=1'>1d6 + [Number]</span> for input prompts)\",\n                \"with extended display text (+2) and display text (+2)\",\n                \"a special 'hit' version which assumes a d20 is to be rolled +7\",\n                \"There's also 1d12+3 and -4\",\n                \"scaledamage: 1d6, scaledice: extra amount (1d6)\",\n                \"<span title='Strength'>20 (+5)</span>, <span title='Strength'>+5</span>, and [Animal Handling](rules/skills.md#Animal%20Handling) (+5)\",\n                \"with an Intelligence of +3 (16), a Wisdom of +0 (10), and a Charisma of +4 (18)\",\n                \"+3 plus PB to hit;  *Hit:* 7 (1d6 + 4) piercing damage plus 7 (2d6) poison damage.\",\n                \"Perception (+2) Perception (-2)\",\n                \"hit display text: +3 to hit\",\n                \"*Melee Weapon Attack:* +9 to hit, reach 5 ft., one target. *Hit:* 9 (1d8 + 5) piercing damage plus 7 (2d6) necrotic damage.\");\n\n        // Now we'll indicate that we're within a trait (for a statblock)\n        boolean pushed = parseState().pushTrait();\n        try {\n            for (int i = 0; i < example.size(); i++) {\n                String result = this.replaceText(example.get(i));\n                assertThat(result).isEqualTo(traits.get(i));\n            }\n\n            // We should get the same result for disabledUsingFS (no backticks)\n            configurator.setUseDiceRoller(DiceRoller.disabledUsingFS);\n            for (int i = 0; i < example.size(); i++) {\n                String result = this.replaceText(example.get(i));\n                assertThat(result).isEqualTo(traits.get(i));\n            }\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    @Test\n    void testSimplify() {\n        String example = \" 7 (`dice:2d6|avg|noform` (`2d6`))\";\n        String result = this.simplifyFormattedDiceText(example);\n        assertThat(result).isEqualTo(\" `dice:2d6|avg|noform|text(7)` (`2d6`)\");\n    }\n\n    @Test\n    void testSimplifyTable() {\n        boolean pushed = parseState().pushMarkdownTable(true);\n        try {\n            String example = \" 7 (`dice:2d6|avg|noform` (`2d6`))\";\n            String result = this.simplifyFormattedDiceText(example);\n            assertThat(result).isEqualTo(\" `dice:2d6\\\\|avg\\\\|noform\\\\|text(7)` (`2d6`)\");\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    @Test\n    void testPlainD20() {\n        Configurator configurator = new Configurator(tui);\n\n        String d20 = \"{@dice d20}\";\n        String oneD20 = \"{@dice 1d20}\";\n        String tag20 = \"{@d20}\";\n\n        assertThat(this.replaceText(d20)).isEqualTo(\"`d20`\");\n        assertThat(this.replaceText(oneD20)).isEqualTo(\"`1d20`\");\n        assertThat(this.replaceText(tag20)).isEqualTo(\"`d20`\");\n\n        configurator.setUseDiceRoller(DiceRoller.enabled);\n\n        assertThat(this.replaceText(d20)).isEqualTo(\"`dice:1d20|noform|noparens|avg|text(d20)`\");\n        assertThat(this.replaceText(oneD20)).isEqualTo(\"`dice:1d20|noform|noparens|avg|text(1d20)`\");\n        assertThat(this.replaceText(tag20)).isEqualTo(\"`dice:1d20|noform|noparens|avg|text(d20)`\");\n\n        configurator.setUseDiceRoller(DiceRoller.enabledUsingFS);\n\n        assertThat(this.replaceText(d20)).isEqualTo(\"`dice:1d20|noform|noparens|avg|text(d20)`\");\n        assertThat(this.replaceText(oneD20)).isEqualTo(\"`dice:1d20|noform|noparens|avg|text(1d20)`\");\n        assertThat(this.replaceText(tag20)).isEqualTo(\"`dice:1d20|noform|noparens|avg|text(d20)`\");\n\n        boolean pushed = parseState().pushMarkdownTable(true);\n        try {\n            assertThat(this.replaceText(d20)).isEqualTo(\"`dice:1d20\\\\|noform\\\\|noparens\\\\|avg\\\\|text(d20)`\");\n            assertThat(this.replaceText(oneD20)).isEqualTo(\"`dice:1d20\\\\|noform\\\\|noparens\\\\|avg\\\\|text(1d20)`\");\n            assertThat(this.replaceText(tag20)).isEqualTo(\"`dice:1d20\\\\|noform\\\\|noparens\\\\|avg\\\\|text(d20)`\");\n        } finally {\n            parseState().pop(pushed);\n        }\n\n        pushed = parseState().pushTrait();\n        try {\n            assertThat(this.replaceText(d20)).isEqualTo(\"d20\");\n            assertThat(this.replaceText(oneD20)).isEqualTo(\"1d20\");\n            assertThat(this.replaceText(tag20)).isEqualTo(\"d20\");\n\n            configurator.setUseDiceRoller(DiceRoller.disabledUsingFS);\n\n            assertThat(this.replaceText(d20)).isEqualTo(\"d20\");\n            assertThat(this.replaceText(oneD20)).isEqualTo(\"1d20\");\n            assertThat(this.replaceText(tag20)).isEqualTo(\"d20\");\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    @Test\n    void testPlainD12() {\n        Configurator configurator = new Configurator(tui);\n\n        String d12 = \"{@dice d12}\";\n        String oneD12 = \"{@dice 1d12}\";\n\n        assertThat(this.replaceText(d12)).isEqualTo(\"`d12`\");\n        assertThat(this.replaceText(oneD12)).isEqualTo(\"`1d12`\");\n\n        configurator.setUseDiceRoller(DiceRoller.enabled);\n\n        assertThat(this.replaceText(d12)).isEqualTo(\"`dice:1d12|noform|noparens|avg|text(d12)`\");\n        assertThat(this.replaceText(oneD12)).isEqualTo(\"`dice:1d12|noform|noparens|avg|text(1d12)`\");\n\n        configurator.setUseDiceRoller(DiceRoller.enabledUsingFS);\n\n        assertThat(this.replaceText(d12)).isEqualTo(\"`dice:1d12|noform|noparens|avg|text(d12)`\");\n        assertThat(this.replaceText(oneD12)).isEqualTo(\"`dice:1d12|noform|noparens|avg|text(1d12)`\");\n\n        boolean pushed = parseState().pushMarkdownTable(true);\n        try {\n            assertThat(this.replaceText(d12)).isEqualTo(\"`dice:1d12\\\\|noform\\\\|noparens\\\\|avg\\\\|text(d12)`\");\n            assertThat(this.replaceText(oneD12)).isEqualTo(\"`dice:1d12\\\\|noform\\\\|noparens\\\\|avg\\\\|text(1d12)`\");\n        } finally {\n            parseState().pop(pushed);\n        }\n\n        pushed = parseState().pushTrait();\n        try {\n            assertThat(this.replaceText(d12)).isEqualTo(\"d12\");\n            assertThat(this.replaceText(oneD12)).isEqualTo(\"1d12\");\n\n            configurator.setUseDiceRoller(DiceRoller.disabledUsingFS);\n\n            assertThat(this.replaceText(d12)).isEqualTo(\"d12\");\n            assertThat(this.replaceText(oneD12)).isEqualTo(\"1d12\");\n        } finally {\n            parseState().pop(pushed);\n        }\n    }\n\n    @Test\n    public void testHitYourSpellAttack() {\n        String s = \"{@hitYourSpellAttack} to hit,\";\n        String result = this.replaceText(s);\n        assertThat(result).isEqualTo(\"your spell attack modifier to hit,\");\n\n        s = \"{@hitYourSpellAttack Bonus equals your spell attack modifier}\";\n        result = this.replaceText(s);\n        assertThat(result).isEqualTo(\"Bonus equals your spell attack modifier\");\n    }\n\n    @Override\n    public Tools5eIndex index() {\n        return index;\n    }\n\n    @Override\n    public Tools5eSources getSources() {\n        return Tools5eSources.findOrTemporary(Tui.MAPPER.createObjectNode());\n    }\n}\n"
  },
  {
    "path": "src/test/java/dev/ebullient/convert/tools/pf2e/CommonDataTests.java",
    "content": "package dev.ebullient.convert.tools.pf2e;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardCopyOption;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.stream.Stream;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.ArrayNode;\n\nimport dev.ebullient.convert.TestUtils;\nimport dev.ebullient.convert.config.CompendiumConfig.Configurator;\nimport dev.ebullient.convert.config.CompendiumConfig.DiceRoller;\nimport dev.ebullient.convert.config.Datasource;\nimport dev.ebullient.convert.config.TtrpgConfig;\nimport dev.ebullient.convert.io.MarkdownWriter;\nimport dev.ebullient.convert.io.Templates;\nimport dev.ebullient.convert.io.Tui;\nimport io.quarkus.arc.Arc;\n\npublic class CommonDataTests {\n    protected final Tui tui;\n    protected final Configurator configurator;\n    protected final Templates templates;\n    protected Pf2eIndex index;\n\n    protected final Path outputPath;\n    protected final TestInput variant;\n\n    enum TestInput {\n        all,\n        partial,\n        none;\n    }\n\n    public CommonDataTests(TestInput variant) throws Exception {\n        tui = Arc.container().instance(Tui.class).get();\n        tui.init(null, !TestUtils.USING_MAVEN, true, true);\n\n        templates = Arc.container().instance(Templates.class).get();\n        tui.setTemplates(templates);\n\n        this.variant = variant;\n        this.outputPath = TestUtils.OUTPUT_ROOT_PF2.resolve(variant.name());\n        tui.setOutputPath(outputPath);\n        outputPath.toFile().mkdirs();\n\n        TtrpgConfig.init(tui, Datasource.toolsPf2e);\n        TtrpgConfig.setToolsPath(TestUtils.PATH_PF2E_TOOLS_DATA);\n\n        configurator = new Configurator(tui);\n\n        index = new Pf2eIndex(TtrpgConfig.getConfig());\n        templates.setCustomTemplates(TtrpgConfig.getConfig());\n\n        if (TestUtils.PATH_PF2E_TOOLS_DATA.toFile().exists()) {\n            switch (variant) {\n                case all:\n                    configurator.allowSource(\"*\");\n                    break;\n                case partial:\n                    configurator.readConfiguration(TestUtils.TEST_RESOURCES.resolve(\"pf2e.json\"));\n                    break;\n                case none:\n                    // should be default (CRD)\n                    break;\n            }\n\n            for (String x : List.of(\"books.json\",\n                    \"book/book-crb.json\", \"book/book-gmg.json\")) {\n                tui.readFile(TestUtils.PATH_PF2E_TOOLS_DATA.resolve(x), TtrpgConfig.getFixes(x), index::importTree);\n            }\n            tui.readToolsDir(TestUtils.PATH_PF2E_TOOLS_DATA, index::importTree);\n            index.prepare();\n        }\n    }\n\n    public void cleanup() throws Exception {\n        configurator.setUseDiceRoller(DiceRoller.disabled);\n        templates.setCustomTemplates(TtrpgConfig.getConfig());\n    }\n\n    public void done() throws IOException {\n        tui.close();\n        Path logFile = Path.of(\"ttrpg-convert.out.txt\");\n        if (Files.exists(logFile)) {\n            Path newFile = outputPath.resolve(logFile);\n            ;\n            Files.move(logFile, newFile, StandardCopyOption.REPLACE_EXISTING);\n        }\n        System.out.println(\"Done.\");\n    }\n\n    public void testDataIndex_pf2e() throws Exception {\n        if (TestUtils.PATH_PF2E_TOOLS_DATA.toFile().exists()) {\n            Path full = outputPath.resolve(\"allIndex.json\");\n            index.writeFullIndex(full);\n\n            Path filtered = outputPath.resolve(\"allSourceIndex.json\");\n            index.writeFilteredIndex(filtered);\n\n            assertThat(full).exists();\n            JsonNode fullIndex = Tui.MAPPER.readTree(full.toFile());\n            ArrayNode fullIndexKeys = fullIndex.withArray(\"keys\");\n            assertThat(fullIndexKeys).isNotNull();\n            assertThat(fullIndexKeys).isNotEmpty();\n            assertThat(fullIndex.has(\"ability|activate an item|crb\"));\n\n            assertThat(filtered).exists();\n            JsonNode filteredIndex = Tui.MAPPER.readTree(filtered.toFile());\n            ArrayNode filteredIndexKeys = filteredIndex.withArray(\"keys\");\n            assertThat(filteredIndexKeys).isNotNull();\n            assertThat(filteredIndexKeys).isNotEmpty();\n\n            if (variant == TestInput.all) {\n                assertThat(fullIndexKeys).isEqualTo(filteredIndexKeys);\n            } else {\n                assertThat(fullIndexKeys.size()).isGreaterThan(filteredIndexKeys.size());\n            }\n        }\n    }\n\n    public void testNotes_p2fe() throws Exception {\n        Path rulesDir = outputPath.resolve(index.rulesFilePath());\n        Path compendiumDir = outputPath.resolve(index.compendiumFilePath());\n\n        if (TestUtils.PATH_PF2E_TOOLS_DATA.toFile().exists()) {\n            MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui);\n            index.markdownConverter(writer)\n                    .writeFiles(Stream.of(Pf2eIndexType.values())\n                            .filter(x -> x.isOutputType() && x.useQuteNote())\n                            .toList())\n                    .writeImages();\n\n            TestUtils.assertDirectoryContents(rulesDir, tui);\n            assertThat(rulesDir.resolve(\"conditions.md\")).exists();\n            assertThat(compendiumDir.resolve(\"skills.md\")).exists();\n        }\n    }\n\n    Path generateNotesForType(Pf2eIndexType type) {\n        return generateNotesForType(List.of(type)).values().iterator().next();\n    }\n\n    Map<Pf2eIndexType, Path> generateNotesForType(List<Pf2eIndexType> types) {\n        Map<Pf2eIndexType, Path> map = new HashMap<>();\n        Set<Path> paths = new HashSet<>();\n        types.forEach(t -> {\n            Path p = outputPath.resolve(t.getFilePath(index))\n                    .resolve(t.relativePath());\n            map.put(t, p);\n            paths.add(p);\n        });\n\n        if (TestUtils.PATH_PF2E_TOOLS_DATA.toFile().exists()) {\n            paths.forEach(p -> TestUtils.deleteDir(p));\n\n            MarkdownWriter writer = new MarkdownWriter(outputPath, templates, tui);\n            index.markdownConverter(writer)\n                    .writeFiles(types);\n\n            paths.forEach(p -> TestUtils.assertDirectoryContents(p, tui));\n        }\n        return map;\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonDataNoneTest.java",
    "content": "package dev.ebullient.convert.tools.pf2e;\n\nimport java.io.IOException;\nimport java.util.List;\n\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\nimport dev.ebullient.convert.config.CompendiumConfig.DiceRoller;\nimport dev.ebullient.convert.tools.pf2e.CommonDataTests.TestInput;\nimport io.quarkus.test.junit.QuarkusTest;\n\n@QuarkusTest\npublic class Pf2eJsonDataNoneTest {\n\n    static CommonDataTests commonTests;\n\n    @BeforeAll\n    public static void setupDir() throws Exception {\n        commonTests = new CommonDataTests(TestInput.none);\n    }\n\n    @AfterAll\n    public static void done() throws IOException {\n        commonTests.done();\n    }\n\n    @AfterEach\n    public void cleanup() throws Exception {\n        commonTests.cleanup();\n    }\n\n    @Test\n    public void testIndex_p2fe() throws Exception {\n        commonTests.testDataIndex_pf2e();\n    }\n\n    @Test\n    public void testAbility_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.ability);\n    }\n\n    @Test\n    public void testAction_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.action);\n    }\n\n    @Test\n    public void testAffliction_p2fe() throws Exception {\n        commonTests.generateNotesForType(List.of(Pf2eIndexType.curse, Pf2eIndexType.disease));\n    }\n\n    @Test\n    public void testArchetype_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.archetype);\n    }\n\n    @Test\n    public void testBackground_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.background);\n    }\n\n    @Test\n    public void testCreature_pf2e() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.creature);\n    }\n\n    @Test\n    public void testDeity_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.deity);\n    }\n\n    @Test\n    public void testDomain_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.domain);\n    }\n\n    @Test\n    public void testFeat_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.feat);\n    }\n\n    @Test\n    public void testHazard_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.hazard);\n    }\n\n    @Test\n    public void testItem_p2fe() throws Exception {\n        commonTests.configurator.setUseDiceRoller(DiceRoller.enabled);\n        commonTests.generateNotesForType(Pf2eIndexType.item);\n    }\n\n    @Test\n    public void testRitual_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.ritual);\n    }\n\n    @Test\n    public void testSpell_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.spell);\n    }\n\n    @Test\n    public void testTable_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.table);\n    }\n\n    @Test\n    public void testTrait_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.trait);\n    }\n\n    @Test\n    public void testNotes_p2fe() throws Exception {\n        commonTests.testNotes_p2fe();\n    }\n}\n"
  },
  {
    "path": "src/test/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonDataSubsetTest.java",
    "content": "package dev.ebullient.convert.tools.pf2e;\n\nimport java.io.IOException;\nimport java.util.List;\n\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\nimport dev.ebullient.convert.config.CompendiumConfig.DiceRoller;\nimport dev.ebullient.convert.tools.pf2e.CommonDataTests.TestInput;\nimport io.quarkus.test.junit.QuarkusTest;\n\n@QuarkusTest\npublic class Pf2eJsonDataSubsetTest {\n\n    static CommonDataTests commonTests;\n\n    @BeforeAll\n    public static void setupDir() throws Exception {\n        commonTests = new CommonDataTests(TestInput.partial);\n    }\n\n    @AfterAll\n    public static void done() throws IOException {\n        commonTests.done();\n    }\n\n    @AfterEach\n    public void cleanup() throws Exception {\n        commonTests.cleanup();\n    }\n\n    @Test\n    public void testIndex_p2fe() throws Exception {\n        commonTests.testDataIndex_pf2e();\n    }\n\n    @Test\n    public void testAbility_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.ability);\n    }\n\n    @Test\n    public void testAction_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.action);\n    }\n\n    @Test\n    public void testAffliction_p2fe() throws Exception {\n        commonTests.generateNotesForType(List.of(Pf2eIndexType.curse, Pf2eIndexType.disease));\n    }\n\n    @Test\n    public void testArchetype_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.archetype);\n    }\n\n    @Test\n    public void testBackground_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.background);\n    }\n\n    @Test\n    public void testCreature_pf2e() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.creature);\n    }\n\n    @Test\n    public void testDeity_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.deity);\n    }\n\n    @Test\n    public void testDomain_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.domain);\n    }\n\n    @Test\n    public void testFeat_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.feat);\n    }\n\n    @Test\n    public void testHazard_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.hazard);\n    }\n\n    @Test\n    public void testItem_p2fe() throws Exception {\n        commonTests.configurator.setUseDiceRoller(DiceRoller.enabled);\n        commonTests.generateNotesForType(Pf2eIndexType.item);\n    }\n\n    @Test\n    public void testRitual_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.ritual);\n    }\n\n    @Test\n    public void testSpell_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.spell);\n    }\n\n    @Test\n    public void testTable_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.table);\n    }\n\n    @Test\n    public void testTrait_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.trait);\n    }\n\n    @Test\n    public void testNotes_p2fe() throws Exception {\n        commonTests.testNotes_p2fe();\n    }\n}\n"
  },
  {
    "path": "src/test/java/dev/ebullient/convert/tools/pf2e/Pf2eJsonDataTest.java",
    "content": "package dev.ebullient.convert.tools.pf2e;\n\nimport java.io.IOException;\nimport java.util.List;\n\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\nimport dev.ebullient.convert.config.CompendiumConfig.DiceRoller;\nimport dev.ebullient.convert.tools.pf2e.CommonDataTests.TestInput;\nimport io.quarkus.test.junit.QuarkusTest;\n\n@QuarkusTest\npublic class Pf2eJsonDataTest {\n\n    static CommonDataTests commonTests;\n\n    @BeforeAll\n    public static void setupDir() throws Exception {\n        commonTests = new CommonDataTests(TestInput.all);\n    }\n\n    @AfterAll\n    public static void done() throws IOException {\n        commonTests.done();\n    }\n\n    @AfterEach\n    public void cleanup() throws Exception {\n        commonTests.cleanup();\n    }\n\n    @Test\n    public void testIndex_p2fe() throws Exception {\n        commonTests.testDataIndex_pf2e();\n    }\n\n    @Test\n    public void testAbility_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.ability);\n    }\n\n    @Test\n    public void testAction_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.action);\n    }\n\n    @Test\n    public void testAffliction_p2fe() throws Exception {\n        commonTests.generateNotesForType(List.of(Pf2eIndexType.curse, Pf2eIndexType.disease, Pf2eIndexType.affliction));\n    }\n\n    @Test\n    public void testArchetype_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.archetype);\n    }\n\n    @Test\n    public void testBackground_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.background);\n    }\n\n    @Test\n    public void testCreature_pf2e() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.creature);\n    }\n\n    @Test\n    public void testDeity_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.deity);\n    }\n\n    @Test\n    public void testDomain_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.domain);\n    }\n\n    @Test\n    public void testFeat_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.feat);\n    }\n\n    @Test\n    public void testHazard_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.hazard);\n    }\n\n    @Test\n    public void testItem_p2fe() throws Exception {\n        commonTests.configurator.setUseDiceRoller(DiceRoller.enabled);\n        commonTests.generateNotesForType(Pf2eIndexType.item);\n    }\n\n    @Test\n    public void testRitual_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.ritual);\n    }\n\n    @Test\n    public void testSpell_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.spell);\n    }\n\n    @Test\n    public void testTable_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.table);\n    }\n\n    @Test\n    public void testTrait_p2fe() throws Exception {\n        commonTests.generateNotesForType(Pf2eIndexType.trait);\n    }\n\n    @Test\n    public void testNotes_p2fe() throws Exception {\n        commonTests.testNotes_p2fe();\n    }\n}\n"
  },
  {
    "path": "src/test/resources/5e/ermis-bg.json",
    "content": "{\n  \"$schema\": \"https://raw.githubusercontent.com/TheGiddyLimit/5etools-utils/master/schema/brew-fast/homebrew.json\",\n  \"_meta\": {\n    \"sources\": [\n      {\n        \"json\": \"ermis-bg\",\n        \"abbreviation\": \"EB\",\n        \"full\": \"Ermis Backgrounds\",\n        \"authors\": [\n          \"LichLife\"\n        ],\n        \"version\": \"1.0.0\"\n      }\n    ],\n    \"dateAdded\": 0,\n    \"dateLastModified\": 0\n  },\n  \"background\": [\n    {\n      \"name\": \"Renegade\",\n      \"source\": \"ermis-bg\",\n      \"skillProficiencies\": [\n        {\n          \"survival\": true,\n          \"choose\": {\n            \"from\": [\n              \"arcana\",\n              \"deception\",\n              \"insight\",\n              \"persuasion\",\n              \"religion\"\n            ],\n            \"count\": 1\n          }\n        }\n      ],\n      \"toolProficiencies\": [\n        {\n          \"anyArtisansTool\": 1,\n          \"choose\": {\n            \"from\": [\n              \"disguise kit\",\n              \"forgery kit\",\n              \"thieves' tools\"\n            ],\n            \"count\": 1\n          }\n        }\n      ],\n      \"languageProficiencies\": [\n        {\n          \"anyStandard\": 1\n        }\n      ],\n      \"entries\": [\n        {\n          \"type\": \"list\",\n          \"style\": \"list-hang-notitle\",\n          \"items\": [\n            {\n              \"type\": \"item\",\n              \"name\": \"Skill Proficiencies:\",\n              \"entry\": \"{@skill Survival} and another of your choice from {@skill Arcana}, {@skill Deception}, {@skill Insight}, {@skill Persuasion}, or {@skill Religion}.\"\n            },\n            {\n              \"type\": \"item\",\n              \"name\": \"Languages:\",\n              \"entry\": \"Two of your choice\"\n            },\n            {\n              \"type\": \"item\",\n              \"name\": \"Tool Proficiencies:\",\n              \"entry\": \"Any {@item Artisan's tools|phb} and one from {@item Disguise Kit|phb}, {@item Forgery Kit|phb}, or {@item Thieves' Tools|phb}.\"\n            }\n          ]\n        },\n        {\n          \"name\": \"Life on the Run\",\n          \"type\": \"entries\",\n          \"entries\": [\n            \"You have lived your life on the run, constantly avoiding capture by the group or association you escaped years ago. Adopting false identities has become second nature to you. Few know your real name or your past. You may be seeking to undermine the group you came from. Or you may simply be trying to live out your life unperturbed by their interference.\"\n          ]\n        }\n      ],\n      \"hasFluff\": true\n    }\n  ]\n}\n"
  },
  {
    "path": "src/test/resources/5e/images-from-local.json",
    "content": "{\n  \"images\": {\n    \"copyInternal\": true,\n    \"internalRoot\" : \"sources/5etools-img\"\n  }\n}\n"
  },
  {
    "path": "src/test/resources/5e/images-remote.json",
    "content": "{\n  \"images\": {\n    \"copyInternal\": false,\n    \"copyExternal\": false,\n    \"internalRoot\" : \"sources/5etools-img\"\n  }\n}\n"
  },
  {
    "path": "src/test/resources/5e/psion.json",
    "content": "{\n  \"$schema\": \"https://raw.githubusercontent.com/TheGiddyLimit/5etools-utils/master/schema/brew-fast/homebrew.json\",\n  \"_meta\": {\n    \"sources\": [\n      {\n        \"json\": \"Psion\",\n        \"abbreviation\": \"Psion\",\n        \"full\": \"LL Psion\",\n        \"authors\": [\n          \"LichLife\"\n        ],\n        \"version\": \"1.0\",\n        \"targetSchema\": \"1.0\"\n      }\n    ],\n    \"optionalFeatureTypes\": {\n      \"AP\": \"Aptitude\",\n      \"minT\": \"Minor Talent\"\n    },\n    \"dateAdded\": 0,\n    \"dateLastModified\": 0\n  },\n  \"skill\": [\n    {\n      \"name\": \"Psionics\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"Psionics is an Intelligence based skill that aids in the identification of Psionic objects, phenomena and creatures.\"\n      ]\n    }\n  ],\n  \"class\": [\n    {\n      \"name\": \"Psion\",\n      \"source\": \"Psion\",\n      \"fluff\": [\n        {\n          \"name\": \"Psion\",\n          \"source\": \"Psion\",\n          \"type\": \"section\",\n          \"entries\": [\n            {\n              \"type\": \"image\",\n              \"href\": {\n                \"type\": \"external\",\n                \"url\": \"file:///Users/roambot/Documents/!Process/lib/media/psion.png\"\n              }\n            },\n            \"The cloaked figure stood amidst a bustling marketplace, their eyes closed in deep concentration. With a gentle sweep of their hand, the figure extended their consciousness beyond the physical realm, delving into the thoughts and emotions of those nearby. The hustle and bustle of the market faded into whispers as they effortlessly read the minds of the crowd, discerning secrets and desires. Knowledge was their weapon in navigating this intricate web of minds, their insights guiding their every move in this world of hidden intentions.\",\n            \"The young woman fled into the forest, consumed with the terror of what she had done. For as long as she could remember, there was something within her mind that she struggled to keep bottled up. That morning, in a moment of weakness, her focus slipped, and a torrent of wild psionic power erupted from within her. She had awoken in the rubble of her home to find her family buried in the ruins. Deep down, she had always known this power would break free.\",\n            {\n              \"type\": \"entries\",\n              \"name\": \"Creating Your Psion\",\n              \"entries\": [\n                \"When creating your Psion, consider how you first discovered and manifested your psychic abilities. Was it in your youth during a moment of intense emotion? Perhaps you first expressed your power through peaceful meditation? Perhaps you were trained from birth to be initiated into a powerful psychic society.\",\n                \"Consider whether you have studied with a psionic master or merely learned on your own to manipulate the psychic powers you find within.\",\n                \"How do you feel about your psionic gift? Is it a point of great pride? Something that separates you from the commoners? Or, do you view your gift as a curse to be hidden and used in the direst situations?\",\n                {\n                  \"type\": \"entries\",\n                  \"name\": \"Quick Build\",\n                  \"page\": 1,\n                  \"entries\": [\n                    \"You can make a Psion quickly by using these suggestions. First, make Constitution your highest ability score, followed by Wisdom or Dexterity. Second, choose the {@background hermit} background. Third, choose the Psi Bolt and ESP minor talents.\"\n                  ]\n                }\n              ]\n            }\n          ]\n        }\n      ],\n      \"hd\": {\n        \"number\": 1,\n        \"faces\": 8\n      },\n      \"proficiency\": [\n        \"con\",\n        \"wis\"\n      ],\n      \"startingProficiencies\": {\n        \"armor\": [\n          \"light\"\n        ],\n        \"weapons\": [\n          \"simple\",\n          \"{@item shortsword|phb|shortswords}\",\n          \"{@item dagger|phb|daggers}\"\n        ],\n        \"skills\": [\n          {\n            \"psionics\": true,\n            \"choose\": {\n              \"from\": [\n                \"athletics\",\n                \"deception\",\n                \"history\",\n                \"insight\",\n                \"intimidation\",\n                \"investigation\",\n                \"medicine\",\n                \"perception\",\n                \"persuasion\",\n                \"religion\"\n              ],\n              \"count\": 2\n            }\n          }\n        ]\n      },\n      \"startingEquipment\": {\n        \"additionalFromBackground\": true,\n        \"default\": [\n          \"(a) a {@item shortsword|phb} or (b) a {@item dagger|phb}\",\n          \"{@item leather armor|phb} or (b) a {@item scale mail|phb} (if proficient)\",\n          \"(a) a {@item scholar's pack|phb} or (b) an {@item explorer's pack|phb}\"\n        ],\n        \"defaultData\": [\n          {\n            \"a\": [\n              \"shortsword|phb\"\n            ],\n            \"b\": [\n              \"dagger|phb\"\n            ]\n          },\n          {\n            \"a\": [\n              \"leather armor|phb\"\n            ],\n            \"b\": [\n              \"studded leather|phb\"\n            ]\n          },\n          {\n            \"a\": [\n              \"scholar's pack|phb\"\n            ],\n            \"b\": [\n              \"explorer's pack|phb\"\n            ]\n          }\n        ]\n      },\n      \"multiclassing\": {\n        \"requirements\": {\n          \"con\": 15,\n          \"wis\": 13\n        },\n        \"proficienciesGained\": {\n          \"armor\": [\n            \"light\"\n          ],\n          \"skills\": [\n            {\n              \"psionics\": true,\n              \"choose\": {\n                \"from\": [\n                  \"athletics\",\n                  \"deception\",\n                  \"history\",\n                  \"insight\",\n                  \"intimidation\",\n                  \"investigation\",\n                  \"medicine\",\n                  \"perception\",\n                  \"persuasion\",\n                  \"religion\"\n                ],\n                \"count\": 2\n              }\n            }\n          ]\n        }\n      },\n      \"classTableGroups\": [\n        {\n          \"colLabels\": [\n            \"PE die\",\n            \"Aptitude\",\n            \"Maj Talent\",\n            \"Min Talent\"\n          ],\n          \"rows\": [\n            [\n              \"1d4\",\n              \"-\",\n              \"-\",\n              \"2\"\n            ],\n            [\n              \"1d6\",\n              \"-\",\n              \"-\",\n              \"2\"\n            ],\n            [\n              \"1d6\",\n              \"1\",\n              \"2\",\n              \"2\"\n            ],\n            [\n              \"1d6\",\n              \"1\",\n              \"2\",\n              \"3\"\n            ],\n            [\n              \"1d8\",\n              \"1\",\n              \"3\",\n              \"3\"\n            ],\n            [\n              \"1d8\",\n              \"1\",\n              \"3\",\n              \"3\"\n            ],\n            [\n              \"1d8\",\n              \"1\",\n              \"3\",\n              \"3\"\n            ],\n            [\n              \"1d8\",\n              \"2\",\n              \"5\",\n              \"3\"\n            ],\n            [\n              \"1d8\",\n              \"2\",\n              \"5\",\n              \"3\"\n            ],\n            [\n              \"1d8\",\n              \"2\",\n              \"5\",\n              \"4\"\n            ],\n            [\n              \"1d10\",\n              \"2\",\n              \"6\",\n              \"4\"\n            ],\n            [\n              \"1d10\",\n              \"2\",\n              \"6\",\n              \"4\"\n            ],\n            [\n              \"1d10\",\n              \"2\",\n              \"6\",\n              \"4\"\n            ],\n            [\n              \"1d10\",\n              \"2\",\n              \"7\",\n              \"4\"\n            ],\n            [\n              \"1d10\",\n              \"2\",\n              \"7\",\n              \"4\"\n            ],\n            [\n              \"1d10\",\n              \"2\",\n              \"7\",\n              \"4\"\n            ],\n            [\n              \"1d12\",\n              \"3\",\n              \"9\",\n              \"4\"\n            ],\n            [\n              \"1d12\",\n              \"3\",\n              \"9\",\n              \"4\"\n            ],\n            [\n              \"1d12\",\n              \"3\",\n              \"9\",\n              \"4\"\n            ],\n            [\n              \"1d12\",\n              \"3\",\n              \"9\",\n              \"4\"\n            ]\n          ]\n        }\n      ],\n      \"classFeatures\": [\n        \"Psionics|Psion|Psion|1|Psion\",\n        \"Psi-Boosted Ability|Psion|Psion|1|Psion\",\n        \"Psi-Damaging Strike|Psion|Psion|1|\",\n        \"Minor Talent|Psion|Psion|1|Psion\",\n        \"Mental Discipline|Psion|Psion|2|Psion\",\n        {\n          \"classFeature\": \"Psionic Archetype|Psion|Psion|3|Psion\",\n          \"gainSubclassFeature\": true\n        },\n        \"Ability Score Improvement|Psion|Psion|4\",\n        {\n          \"classFeature\": \"Psionic Archetype Feature|Psion|Psion|5|Psion\",\n          \"gainSubclassFeature\": true\n        },\n        \"Channel Life Force|Psion|Psion|6|Psion\",\n        \"Ability Score Improvement|Psion|Psion|7|Psion\",\n        {\n          \"classFeature\": \"Psionic Archetype Feature|Psion|Psion|9|Psion\",\n          \"gainSubclassFeature\": true\n        },\n        \"Ability Score Improvement|Psion|Psion|10|Psion\",\n        \"Ability Score Improvement|Psion|Psion|12|Psion\",\n        \"Empowered Psionics|Psion|Psion|13|Psion|Psion\",\n        {\n          \"classFeature\": \"Psionic Archetype Feature|Psion|Psion|15|Psion\",\n          \"gainSubclassFeature\": true\n        },\n        \"Ability Score Improvement|Psion|Psion|16\",\n        \"Enhanced Innate Psionics|Psion|Psion|18|Psion\",\n        \"Ability Score Improvement|Psion|Psion|19\",\n        \"Embodied Mind|Psion|Psion|20|Psion\"\n      ],\n      \"foundryAdvancement\": [\n        {\n          \"type\": \"ScaleValue\",\n          \"configuration\": {\n            \"identifier\": \"talent-scale\",\n            \"type\": \"dice\",\n            \"scale\": {\n              \"1\": {\n                \"number\": 1,\n                \"die\": 4\n              },\n              \"2\": {\n                \"number\": 1,\n                \"die\": 6\n              },\n              \"5\": {\n                \"number\": 2,\n                \"die\": 8\n              },\n              \"11\": {\n                \"number\": 3,\n                \"die\": 10\n              },\n              \"17\": {\n                \"number\": 4,\n                \"die\": 12\n              }\n            }\n          },\n          \"title\": \"Talent PED Scale\",\n          \"classRestriction\": \"primary\"\n        },\n        {\n          \"type\": \"ItemChoice\",\n          \"configuration\": {\n            \"choices\": {\n              \"3\": 1,\n              \"8\": 1,\n              \"17\": 1\n            },\n            \"allowDrops\": true,\n            \"type\": null,\n            \"pool\": [],\n            \"restriction\": {\n              \"type\": \"class\",\n              \"subtype\": \"psionicPower\"\n            },\n            \"hint\": \"Choose an aptitude.\"\n          },\n          \"value\": {},\n          \"title\": \"Aptitude\",\n          \"icon\": null\n        },\n        {\n          \"type\": \"ItemChoice\",\n          \"configuration\": {\n            \"choices\": {\n              \"3\": 2,\n              \"5\": 1,\n              \"8\": 2,\n              \"11\": 1,\n              \"14\": 1,\n              \"17\": 2\n            },\n            \"allowDrops\": true,\n            \"type\": \"feat\",\n            \"pool\": [],\n            \"hint\": \"Choose major talents.\"\n          },\n          \"title\": \"Major Talents\",\n          \"icon\": null\n        },\n        {\n          \"type\": \"ItemChoice\",\n          \"configuration\": {\n            \"choices\": {\n              \"1\": 2,\n              \"4\": 1,\n              \"10\": 1\n            },\n            \"allowDrops\": true,\n            \"type\": \"feat\",\n            \"pool\": [],\n            \"hint\": \"Choose minor talents.\"\n          },\n          \"value\": {},\n          \"title\": \"Minor Talents\",\n          \"icon\": null\n        }\n      ]\n    }\n  ],\n  \"classFeature\": [\n    {\n      \"name\": \"Psionics\",\n      \"source\": \"Psion\",\n      \"page\": 1,\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"level\": 1,\n      \"entries\": [\n        \"Your have psionic abilities. The psionic energy that you possess is represented by your Psionic Energy Die (PE die). The powers that you have in virtue of this psionic energy are determined by the Aptitudes and Talents you have chosen as you progress in this class. Psionics is distinct from magic, though it shares in some of its effects.\",\n        {\n          \"type\": \"entries\",\n          \"name\": \"Psionic Energy Die\",\n          \"entries\": [\n            \"As a Psion you harbor a wellspring of psionic power within yourself, as represented by your Psionic Energy die, the starting size of which at level 1 is  {@dice 1d4}.\",\n            \"Whenever you finish a long rest, your Psionic Energy die resets to its starting size. When you reach certain levels in this class, the starting size of your Psionic Energy die increases: at 2nd level ({@dice d6}), 5th level ({@dice d8}), 11th level ({@dice d10}), and 17th level ({@dice d12}).\",\n            \"{@b Changing the Die's Size.} If you roll the highest number on your Psionic Energy die, it decreases by one die size after the roll. This represents you burning through your psionic energy. For example, if the die is a {@dice d6} and you roll a 6, it becomes a {@dice d4}. If it's a {@dice d4} and you roll a 4, it becomes unusable until you finish a long rest.\",\n            \"Conversely, if you roll a 1 on your Psionic Energy die, it increases by one die size after the roll, up to its initial size. This represents you conserving psionic energy for later use. For example, if you roll a 1 on a {@dice d4}, the die then becomes a {@dice d6}. Your PE die can never increase in size beyond your level's initial starting size.\"\n          ]\n        },\n        {\n          \"type\": \"entries\",\n          \"name\": \"Psionic Aptitudes\",\n          \"entries\": [\n            \"Aptitudes are generic forms of the expression of psychic power. Each aptitude allows a character to engage in more specific forms of psychic activity called 'talents'. There are five different aptitudes, each with their own distinctive major talents. These are Scanning, Sending, Porting, Changing, Nullifying, and Energizing. Each aptitude has its own distinctive talents. You gain aptitudes at levels 3, 8, and 17. The Psion progression table shows the total number of aptitudes you have at each level.\"\n          ]\n        },\n        {\n          \"type\": \"entries\",\n          \"name\": \"Psionic Talents\",\n          \"entries\": [\n            \"While aptitudes are generic forms of the expression of psychic power, each aptitude allows a character to engage in more specific forms of psychic activity called 'talents'. There are two classes of talents. 'Major' talents are those that are specific to (and only accessible to those with) an aptitude. 'Minor' talents are those talents that may be acquired {@i regardless} of aptitude. You begin with two minor talents.\",\n            \"Major talents are acquired at levels 3, 8, 11, 14, and 18. You cannot choose a major talent unless you have the relevant aptitude. Also, unless otherwise specified, the use of a major talent requires the expenditure of a PE die size. If you have no PE die sizes remaining then you cannot use the major talent.\"\n          ]\n        },\n        {\n          \"type\": \"entries\",\n          \"name\": \"Psionic Ability\",\n          \"entries\": [\n            \"Constitution is your ability for your psionic powers, since the source of your power comes from the strength of your lifeforce. You use your Constitution whenever an exercise of your psionic power refers to your psionic ability. In addition, you use your Constitution modifier when setting the saving throw DC for a psionic talent or a spell whose effects you recreate with your psionic abilities, and when making an attack roll that uses a psionic ability.\",\n            {\n              \"type\": \"abilityDc\",\n              \"name\": \"Psion\",\n              \"attributes\": [\n                \"con\"\n              ]\n            },\n            {\n              \"type\": \"abilityAttackMod\",\n              \"name\": \"Psion\",\n              \"attributes\": [\n                \"con\"\n              ]\n            }\n          ]\n        }\n      ]\n    },\n    {\n      \"name\": \"Minor Talent\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"level\": 1,\n      \"entries\": [\n        \"'Minor' talents are those talents that may be acquired {@i regardless} of aptitude. You being with two minor talents. You gain an additional minor talent at levels 4 and 10.\"\n      ]\n    },\n    {\n      \"name\": \"Psi-Boosted Ability\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"level\": 1,\n      \"entries\": [\n        \"At 1st level, you can use your psionic powers to boost your abilities. You can roll your Psionic Energy die and add the number rolled to an ability check with the chosen ability. You can do this a number of times equal to your proficiency bonus and regain all uses after a short or long rest.\"\n      ]\n    },\n    {\n      \"name\": \"Psi-Damaging Strike\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"level\": 1,\n      \"entries\": [\n        \"Starting at 1st level, when you hit with an attack roll, you can roll your Psionic Energy die and replace one of the damage dice with the number rolled on the die. You can wait to do this until {@i after} you've rolled the original attack damage. The damage from you PED strike is {@b force} damage.\"\n      ]\n    },\n    {\n      \"name\": \"Psi Replenishment\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"level\": 1,\n      \"entries\": [\n        \"As a bonus action, you can calm your mind for a moment and restore your Psionic Energy die to its maximum size. You can't use Psi Replenishment again until you finish a long rest.\"\n      ]\n    },\n    {\n      \"name\": \"Mental Discipline\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"level\": 2,\n      \"entries\": [\n        \"At 2nd level, you can use your reaction to a psychic attack and become resistant to all psychic damage from that attacker until the end of their next turn. You also have advantage on saving throws against attempts to manipulate your mind or subject you to conditions such as charm, fear, or confusion. As a bonus action, you can expend a Psionic Energy die size to end one effect on yourself that subjects you to a condition.\"\n      ]\n    },\n    {\n      \"name\": \"Psionic Archetype\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"level\": 3,\n      \"entries\": [\n        \"Choose a psionic archetype. At 3rd, 5th, 9th, and 15th levels, you gain abilities specific to your psionic archetype.\"\n      ]\n    },\n    {\n      \"name\": \"Ability Score Improvement\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"level\": 4,\n      \"entries\": [\n        \"When you reach 4th level, you can increase one ability score of your choice by 2, or you can increase two ability scores of your choice by 1. As normal, you can't increase an ability score above 20 using this feature.\",\n        \"If your DM allows the use of feats, you may instead take a {@5etools feat|feats.html}.\"\n      ]\n    },\n    {\n      \"name\": \"Psionic Archetype Feature\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"level\": 5,\n      \"entries\": [\n        \"You gain abilities specific to your psionic archetype.\"\n      ]\n    },\n    {\n      \"name\": \"Channel Life Force\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"level\": 6,\n      \"entries\": [\n        \"At 6th level, you can channel your life force into psychic energy in two ways: Psi Stamina Drain and Psi Life Conversion.\"\n      ]\n    },\n    {\n      \"name\": \"Ability Score Improvement\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"level\": 7,\n      \"entries\": [\n        \"When you reach 7th level, you can increase one ability score of your choice by 2, or you can increase two ability scores of your choice by 1. As normal, you can't increase an ability score above 20 using this feature.\",\n        \"If your DM allows the use of feats, you may instead take a {@5etools feat|feats.html}.\"\n      ]\n    },\n    {\n      \"name\": \"Psionic Archetype Feature\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"level\": 9,\n      \"entries\": [\n        \"You gain abilities specific to your psionic archetype.\"\n      ]\n    },\n    {\n      \"name\": \"Ability Score Improvement\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"level\": 10,\n      \"entries\": [\n        \"When you reach 10th level, you can increase one ability score of your choice by 2, or you can increase two ability scores of your choice by 1. As normal, you can't increase an ability score above 20 using this feature.\",\n        \"If your DM allows the use of feats, you may instead take a {@5etools feat|feats.html}.\"\n      ]\n    },\n    {\n      \"name\": \"Ability Score Improvement\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"level\": 12,\n      \"entries\": [\n        \"When you reach 12th level, you can increase one ability score of your choice by 2, or you can increase two ability scores of your choice by 1. As normal, you can't increase an ability score above 20 using this feature.\",\n        \"If your DM allows the use of feats, you may instead take a {@5etools feat|feats.html}.\"\n      ]\n    },\n    {\n      \"name\": \"Empowered Psionics\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"level\": 13,\n      \"entries\": [\n        \"At 13th level, when you deal psychic or force damage with a psychic attack, you can add your Constitution modifier to the damage against the attack’s targets.\"\n      ]\n    },\n    {\n      \"name\": \"Psionic Archetype Feature\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"level\": 15,\n      \"entries\": [\n        \"You gain abilities specific to your psionic archetype.\"\n      ]\n    },\n    {\n      \"name\": \"Ability Score Improvement\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"level\": 16,\n      \"entries\": [\n        \"When you reach 16th level, you can increase one ability score of your choice by 2, or you can increase two ability scores of your choice by 1. As normal, you can't increase an ability score above 20 using this feature.\",\n        \"If your DM allows the use of feats, you may instead take a {@5etools feat|feats.html}.\"\n      ]\n    },\n    {\n      \"name\": \"Enhanced Innate Psionics\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"level\": 18,\n      \"entries\": [\n        \"At eighteenth level you gain enhanced innate psionic powers. Once per short or long rest you can cast one of Dominate Person, Scrying, or Telekinesis, and do so without verbal, somatic, or material components.\"\n      ]\n    },\n    {\n      \"name\": \"Ability Score Improvement\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"level\": 19,\n      \"entries\": [\n        \"When you reach 19th level, you can increase one ability score of your choice by 2, or you can increase two ability scores of your choice by 1. As normal, you can't increase an ability score above 20 using this feature.\",\n        \"If your DM allows the use of feats, you may instead take a {@5etools feat|feats.html}.\"\n      ]\n    },\n    {\n      \"name\": \"Embodied Mind\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"level\": 20,\n      \"entries\": [\n        \"At twentieth level your body has fully become a physical manifestation of your psychic prowess. You have resistance to non-magical bludgeoning, piercing, slashing, poison, and force damage. You no longer age, and you are immune to disease. Moreover, you regain psychic energy more efficiently. You may reduce a level of exhaustion in a short rest. When expending hit-dice in a short rest, for each hit-die expended, you may roll your Psionic Energy die and add the result. Note that your PE die is subject to normal rules here concerning expansion or contraction of the die size.\"\n      ]\n    }\n  ],\n  \"subclass\": [\n    {\n      \"name\": \"Wilder\",\n      \"source\": \"Psion\",\n      \"shortName\": \"Wilder\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"subclassFeatures\": [\n        \"Wilder|Psion|Psion|Wilder|Psion|3|Psion\",\n        \"Wild Surge|Psion|Psion|Wilder|Psion|3|Psion\",\n        \"Psychic Feedback|Psion|Psion|Wilder|Psion|5|Psion\",\n        \"Unpredictable Power|Psion|Psion|Wilder|Psion|9|Psion\",\n        \"Psychic Eruption|Psion|Psion|Wilder|Psion|15|Psion\"\n      ]\n    },\n    {\n      \"name\": \"Tactician\",\n      \"source\": \"Psion\",\n      \"shortName\": \"Tactician\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"subclassFeatures\": [\n        \"Tactician|Psion|Psion|Tactician|Psion|3|Psion\",\n        \"Contingency Plan|Psion|Psion|Tactician|Psion|3|Psion\",\n        \"Adaptive Tactics|Psion|Psion|Tactician|Psion|5|Psion\",\n        \"Master Strategist|Psion|Psion|Tactician|Psion|9|Psion\",\n        \"Flawless Execution|Psion|Psion|Tactician|Psion|15|Psion\"\n      ]\n    },\n    {\n      \"name\": \"Inquisitor\",\n      \"source\": \"Psion\",\n      \"shortName\": \"Inquisitor\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"subclassFeatures\": [\n        \"Inquisitor|Psion|Psion|Inquisitor|Psion|3|Psion\",\n        \"Lie Detector|Psion|Psion|Inquisitor|Psion|3|Psion\",\n        \"Mind Probe|Psion|Psion|Inquisitor|Psion|3|Psion\",\n        \"Mind Lock|Psion|Psion|Inquisitor|Psion|5|Psion\",\n        \"Psychic Profiling|Psion|Psion|Inquisitor|Psion|9|Psion\",\n        \"Telepathic Mastery|Psion|Psion|Inquisitor|Psion|15|Psion\"\n      ]\n    }\n  ],\n  \"subclassFeature\": [\n    {\n      \"name\": \"Wilder\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"subclassShortName\": \"Wilder\",\n      \"subclassSource\": \"Psion\",\n      \"level\": 3,\n      \"entries\": [\n        \"Wilders  are powerful psions that lack any formal training. Their abilities can vary, and their use can sometimes be unpredictable. But they tend to make up for their lack of formal training with unusual abilities and a ferociousness of mind. The Wilder subclass has its choice of the aptitudes.\"\n      ]\n    },\n    {\n      \"name\": \"Wild Surge\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"subclassShortName\": \"Wilder\",\n      \"subclassSource\": \"Psion\",\n      \"level\": 3,\n      \"entries\": [\n        \"Allows you to gain temporary hit points and trigger a wild surge when using a psionic talent. The wild surge can have various effects based on a d20 roll, including advantage, additional targets, or maximizing damage.\"\n      ]\n    },\n    {\n      \"name\": \"Psychic Feedback\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"subclassShortName\": \"Wilder\",\n      \"subclassSource\": \"Psion\",\n      \"level\": 5,\n      \"entries\": [\n        \"Lets you send a burst of psychic energy back at attackers when you take damage, potentially causing them to take damage and be stunned.\"\n      ]\n    },\n    {\n      \"name\": \"Unpredictable Power\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"subclassShortName\": \"Wilder\",\n      \"subclassSource\": \"Psion\",\n      \"level\": 9,\n      \"entries\": [\n        \"Allows you to use your Wild Surge as a reaction to influence the outcome of attack rolls, ability checks, or saving throws made by yourself or a creature within 30 feet.\"\n      ]\n    },\n    {\n      \"name\": \"Psychic Eruption\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"subclassShortName\": \"Wilder\",\n      \"subclassSource\": \"Psion\",\n      \"level\": 15,\n      \"entries\": [\n        \"Allows you to unleash a psychic eruption, damaging all creatures within a 60-foot radius and imposing penalties on their rolls.\"\n      ]\n    },\n    {\n      \"name\": \"Tactician\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"subclassShortName\": \"Tactician\",\n      \"subclassSource\": \"Psion\",\n      \"level\": 3,\n      \"entries\": [\n        \"Highly strategic and tactical thinkers, Tacticians are trained to account for every apparent contingency, as well as to adapt to every situation. They do not typically find themselves in battle but largely because their plans to not require them to. The Tactician must choose at least two aptitudes from scanning, sending, porting, and nullifying. The Tactician may not have an aptitude in changing.\"\n      ]\n    },\n    {\n      \"name\": \"Contingency Plan\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"subclassShortName\": \"Tactician\",\n      \"subclassSource\": \"Psion\",\n      \"level\": 3,\n      \"entries\": [\n        \"As a bonus action, you can create a contingency plan for yourself or an ally within 60 feet. The plan can be activated at any time before the end of your next turn and triggers automatically when a specific condition you specify is met (such as 'when an enemy enters a designated area', 'when an ally takes damage from a specific type of attack', etc.). The contingency plan can have various effects, such as granting temporary hit points, teleporting the target to safety, granting advantage on an attack, or causing a specific spell to be cast. You can create a number of contingency plans equal to your proficiency bonus, and you regain all expended uses of this feature after a long rest.\"\n      ]\n    },\n    {\n      \"name\": \"Adaptive Tactics\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"subclassShortName\": \"Tactician\",\n      \"subclassSource\": \"Psion\",\n      \"level\": 5,\n      \"entries\": [\n        \"Whenever you or an ally within 60 feet of you successfully hits a creature with an attack, you can use your reaction to grant them an extra benefit based on the type of attack. If the attack was a melee attack, the target's speed is reduced by half until the end of its next turn. If the attack was a ranged attack, the target has disadvantage on attack rolls until the end of its next turn. If the attack was a spell attack, the target has disadvantage on its next saving throw before the end of its next turn. You can use this feature a number of times equal to your Intelligence modifier (minimum of once), and you regain all expended uses after a short or long rest.\"\n      ]\n    },\n    {\n      \"name\": \"Master Strategist\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"subclassShortName\": \"Tactician\",\n      \"subclassSource\": \"Psion\",\n      \"level\": 9,\n      \"entries\": [\n        \"You gain proficiency in the Wisdom (Perception) and Wisdom (Insight) skills, as well as expertise in one Intelligence-based skill of your choice. In addition, whenever you roll initiative, you can use your reaction to grant yourself and all allies within 60 feet of you advantage on their initiative rolls. You can use this feature once per long rest.\"\n      ]\n    },\n    {\n      \"name\": \"Flawless Execution\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"subclassShortName\": \"Tactician\",\n      \"subclassSource\": \"Psion\",\n      \"level\": 15,\n      \"entries\": [\n        \"When you or an ally within 60 feet of you makes an attack roll, ability check, or saving throw, you can use your reaction to grant them a bonus to the roll equal to your Intelligence modifier (minimum of +1). If the roll still fails, you can choose whether to expend a PE die to have the creature treat the roll as if it had succeeded instead. You can use this feature a number of times equal to your proficiency bonus, and you regain all expended uses after a long rest.\"\n      ]\n    },\n    {\n      \"name\": \"Inquisitor\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"subclassShortName\": \"Inquisitor\",\n      \"subclassSource\": \"Psion\",\n      \"level\": 3,\n      \"entries\": [\n        \"The Inquisitor is a master of scanning and sending. They can capture information from another and send it to whomever needs it. An Inquisitor must choose the scanning and sending aptitudes. It's third aptitude is elective.\"\n      ]\n    },\n    {\n      \"name\": \"Lie Detector\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"subclassShortName\": \"Inquisitor\",\n      \"subclassSource\": \"Psion\",\n      \"level\": 3,\n      \"entries\": [\n        \"You can tell when any creature with whom you are communicating by telepathy is lying to you.\"\n      ]\n    },\n    {\n      \"name\": \"Mind Probe\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"subclassShortName\": \"Inquisitor\",\n      \"subclassSource\": \"Psion\",\n      \"level\": 3,\n      \"entries\": [\n        \"While similar to ESP, a probe targets a single individual within 30 feet of the Psion, and permits much deeper access to a target's mind. If the target fails a Wisdom save all of their memories and knowledge become (in principle) accessible via questioning. The Psion may ask one question per round (any additional questions require expending an PE die size) which the target must reply truthfully or reply with \\\"I don't know.\\\" However, if the question or answer becomes too complicated, the DM may require more than 1 round to complete the action, allow the target an additional Wisdom save, or simply deny the request. The target need not be conscious for this ability to work. The use of this ability requires the Psion's concentration for the duration of the inquiry. You can use this talent a number of times equal to your proficiency bonus, and you regain all uses after a long rest.\"\n      ]\n    },\n    {\n      \"name\": \"Mind Lock\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"subclassShortName\": \"Inquisitor\",\n      \"subclassSource\": \"Psion\",\n      \"level\": 5,\n      \"entries\": [\n        \"Mind Lock is a kind of psychic manipulation that renders inaccessible to any but the Psion a portion of the target's mind. The target must make a successful Wisdom saving throw to avoid this effect. If the saving throw fails, the wipe has been successful, making the knowledge in that part of the target's mind inaccessible, and reducing the target's intelligence and Wisdom scores by 1 point each. This can affect everything from languages known, to spells which may be cast. This memory loss is permanent unless restored by the Psion or through a wish or similar spell. This ability may cumulatively affect a target down to a minimum intelligence or Wisdom score of 3. You can use this talent a number of times equal to your proficiency bonus, and you regain all uses after a long rest.\"\n      ]\n    },\n    {\n      \"name\": \"Psychic Profiling\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"subclassShortName\": \"Inquisitor\",\n      \"subclassSource\": \"Psion\",\n      \"level\": 9,\n      \"entries\": [\n        \"You gain exceptional insight concerning any individual or creature whom you have previously detected via any other scanning or minor talent.\\n\\nFor purposes of this talent, \\\"exceptional insight\\\" includes a name, a mental image of the subject, the subject's alignment, and the subject's location (an image of the subject's current location that is, e.g., good enough to teleport to if studied carefully). Powers, special abilities, and spells do not protect against your profiling ability, not even Mind Blank or spells such as Wish. You can use this talent a number of times equal to your proficiency bonus, and you regain all uses after a long rest.\"\n      ]\n    },\n    {\n      \"name\": \"Telepathic Mastery\",\n      \"source\": \"Psion\",\n      \"className\": \"Psion\",\n      \"classSource\": \"Psion\",\n      \"subclassShortName\": \"Inquisitor\",\n      \"subclassSource\": \"Psion\",\n      \"level\": 15,\n      \"entries\": [\n        \"You have gained mastery over your telepathic abilities. You can use your Mind Link talent to communicate telepathically with any creature that you can immediately perceive, regardless of their distance from you. You can also use your Probe Thoughts talent on any creature you have previously probed thoughts from, regardless of how long ago it was. Finally, when you use your Mind Meld talent, you can choose to expend your Psionic Energy die to increase the duration of the effect by a number of hours equal to the die's size. Additionally, the target creature takes psychic damage equal to twice your Psionic Energy die if it fails its Intelligence saving throw at the end of the effect. You may choose to have it automatically succeed its save. You can use this talent a number of times equal to your proficiency bonus, and you regain all uses after a long rest.\"\n      ]\n    }\n  ],\n  \"optionalfeature\": [\n    {\n      \"name\": \"ESP\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} Special\",\n        \"{@bold Range:} 50 feet\",\n        \"{@bold PE Cost:} Special\",\n        \"As an action, you can perceive any mind that is not protected or otherwise shielded against divination or psionic targeting within 50 feet. Any creature within this area that fails a Wisdom saving throw against your psion DC will make its surface thoughts available to be understood regardless of the language spoken. If the creature has an Intelligence of 3 or less, it will transmit impressionistic \\\"pictures\\\" and/or raw emotions and desires. A creature that is hidden from or otherwise immune to psionics or divination magic is immune to this feature.\"\n      ],\n      \"foundrySystem\": {\n        \"activation\": {\n          \"featureType\": \"action\",\n          \"cost\": \"1\"\n        }\n      },\n      \"featureType\": [\n        \"minT\"\n      ],\n      \"prerequisite\": [\n        {\n          \"feature\": [\n            \"Minor Talent\"\n          ],\n          \"psionics\": true\n        }\n      ]\n    },\n    {\n      \"name\": \"Mental Rapport\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Type:} Bonus Action\",\n        \"{@bold Duration:} Instantaneous\",\n        \"{@bold Range:} 30 feet\",\n        \"{@bold PE Cost:} Special\",\n        \"As a bonus action, you may communicate telepathically with one willing creature within 30 feet. The creature must be able to speak a language that you can understand. Otherwise, only basic sensory images or feelings may be communicated. Any barriers of rock, wood, or metal thicker than a few inches will block your ability. A creature that has an Intelligence score lower than 3 or is hidden from psionics or divination magic is immune to this feature.\"\n      ],\n      \"foundrySystem\": {\n        \"activation\": {\n          \"featureType\": \"bonus\",\n          \"cost\": \"1\"\n        }\n      },\n      \"featureType\": [\n        \"minT\"\n      ],\n      \"prerequisite\": [\n        {\n          \"feature\": [\n            \"Minor Talent\"\n          ],\n          \"psionics\": true\n        }\n      ]\n    },\n    {\n      \"name\": \"Psionic Wave\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} Instantaneous\",\n        \"{@bold Range:} 5 ft. radius centered on self\",\n        \"{@bold PE Cost:} Special\",\n        \"As an action, you release a wave of psionic force energy within a 5 ft. radius centered on you. Each creature in the area must make a Wisdom saving throw against your Psion DC or take {@damage 1d6} force damage on a failed save, or half as much on a successful one.\",\n        \"This attack's damage increases by {@dice 1d6} when you reach 5th level ({@damage 2d6}), 11th level ({@damage 3d6}), and 17th level ({@damage 4d6}).\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"action\",\n        \"activation.cost\": \"1\",\n        \"duration\": {\n          \"value\": \"\",\n          \"units\": \"inst\"\n        },\n        \"actionType\": \"msak\",\n        \"ability\": \"con\",\n        \"target\": {\n          \"value\": 5,\n          \"width\": null,\n          \"units\": \"ft\",\n          \"type\": \"radius\"\n        },\n        \"range\": {\n          \"value\": null,\n          \"long\": null,\n          \"units\": \"ft\"\n        },\n        \"type\": {\n          \"value\": \"class\",\n          \"subtype\": \"psionicPower\"\n        },\n        \"save\": {\n          \"ability\": \"wis\",\n          \"dc\": \"\",\n          \"scaling\": \"spell\"\n        },\n        \"damage\": {\n          \"parts\": [\n            [\n              \"(@scale.psion.talent-scale.number)d6\",\n              \"force\"\n            ]\n          ]\n        }\n      },\n      \"featureType\": [\n        \"minT\"\n      ],\n      \"prerequisite\": [\n        {\n          \"feature\": [\n            \"Minor Talent\"\n          ],\n          \"psionics\": true\n        }\n      ]\n    },\n    {\n      \"name\": \"Psychic Shield\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Type:} Reaction\",\n        \"{@bold Duration:} Until the end of your next turn\",\n        \"{@bold Range:} Self or creature within sight\",\n        \"{@bold PE Cost:} Special\",\n        \"As a reaction, you create a shield of psionic energy that surrounds you or another creature that you can see. Pick one of the following benefits and roll your PE die. The reduction or benefit is equal to the value of the rolled die. The effect lasts until the end of your next turn.\",\n        {\n          \"type\": \"list\",\n          \"items\": [\n            \"Reduction of psychic damage.\",\n            \"Bonus to any saves against divination magic or mental scan.\",\n            \"Bonus to any save against a condition caused by enchantment magics from charm, confusion, fear, etc.\"\n          ]\n        },\n        \"At 5th, 11th, 17th, and 20th level, you may shield an additional creature that you can see, up to a maximum of five.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"reaction\",\n        \"activation.cost\": \"1\"\n      },\n      \"featureType\": [\n        \"minT\"\n      ]\n    },\n    {\n      \"name\": \"Mind Spike\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} -\",\n        \"{@bold Range:} 60 ft\",\n        \"{@bold PE Cost:} Special\",\n        \"You drive a disorienting spike of psychic energy into the mind of one creature you can see within range. The target must succeed on an Intelligence saving throw or take {@damage 1d6} psychic damage and subtract {@dice 1d4} from the next saving throw it makes before the end of your next turn.\",\n        \"This spell's damage increases by {@dice 1d6} when you reach certain levels: 5th level ({@dice 2d6}), 11th level ({@dice 3d6}), and 17th level ({@dice 4d6}).\"\n      ],\n      \"foundrySystem\": {\n        \"activation\": {\n          \"featureType\": \"action\",\n          \"cost\": \"1\"\n        },\n        \"range\": {\n          \"value\": 120,\n          \"units\": \"ft\"\n        },\n        \"duration\": {\n          \"value\": \"\",\n          \"units\": \"inst\"\n        },\n        \"actionType\": \"rsak\",\n        \"ability\": \"con\",\n        \"target\": {\n          \"value\": 1,\n          \"type\": \"creature\"\n        },\n        \"damage\": {\n          \"parts\": [\n            [\n              \"(@scale.psion.talent-scale.number)d6\",\n              \"psychic\"\n            ]\n          ]\n        }\n      },\n      \"featureType\": [\n        \"minT\"\n      ],\n      \"prerequisite\": [\n        {\n          \"feature\": [\n            \"Minor Talent\"\n          ],\n          \"psionics\": true\n        }\n      ]\n    },\n    {\n      \"name\": \"Psychic Whip\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Type:} Bonus Action\",\n        \"{@bold Duration:} Instantaneous\",\n        \"{@bold Range:} 30 feet\",\n        \"{@bold PE Cost:} Special\",\n        \"As a bonus action, you create a psychic whip that lashes out at a target within 30 feet. The target must make a Wisdom saving throw or take {@damage 1d4} psychic damage and be pulled up to 10 feet towards you.\",\n        \"This attack's damage increases by {@dice 1d4} when you reach 5th level ({@damage 2d4}), 11th level ({@damage 3d4}), and 17th level ({@damage 4d4}).\"\n      ],\n      \"foundrySystem\": {\n        \"activation\": {\n          \"featureType\": \"bonus\",\n          \"cost\": \"1\"\n        }\n      },\n      \"featureType\": [\n        \"minT\"\n      ],\n      \"prerequisite\": [\n        {\n          \"feature\": [\n            \"Minor Talent\"\n          ],\n          \"psionics\": true\n        }\n      ]\n    },\n    {\n      \"name\": \"Toughen\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} Until the end of your next turn\",\n        \"{@bold Range:} Self\",\n        \"{@bold PE Cost:} Special\",\n        \"You use your psionic energy to toughen your body. Until the end of your next turn, you have resistance against bludgeoning, piercing, and slashing damage dealt by weapon attacks.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.featureType\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"featureType\": [\n        \"minT\"\n      ],\n      \"prerequisite\": [\n        {\n          \"feature\": [\n            \"Minor Talent\"\n          ],\n          \"psionics\": true\n        }\n      ]\n    },\n    {\n      \"name\": \"Psi Digits\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} Concentration\",\n        \"{@bold Range:} 30 ft\",\n        \"{@bold PE Cost:} Special\",\n        \"An invisible locus of force can be used as a hand whose digits may manipulate an object, open an unlocked door or container, stow or retrieve an item from an open container, or pour the contents out of a vial. You can move the locus up to 30 feet each time you use it. The locus of force is invisible, but can't attack, activate magic items, or carry more than 10 pounds.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.featureType\": \"action\",\n        \"activation.cost\": \"1\",\n        \"range\": {\n          \"value\": 30,\n          \"units\": \"ft\"\n        }\n      },\n      \"featureType\": [\n        \"minT\"\n      ],\n      \"prerequisite\": [\n        {\n          \"feature\": [\n            \"Minor Talent\"\n          ],\n          \"psionics\": true\n        }\n      ]\n    },\n    {\n      \"name\": \"Life Touch\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} -\",\n        \"{@bold Range:} Touch\",\n        \"{@bold PE Cost:} Special\",\n        \"You touch a living creature that has 0 hit points. The creature becomes stable, as similar to the spell {@spell spare the dying}.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.featureType\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"featureType\": [\n        \"minT\"\n      ],\n      \"prerequisite\": [\n        {\n          \"feature\": [\n            \"Minor Talent\"\n          ],\n          \"psionics\": true\n        }\n      ]\n    },\n    {\n      \"name\": \"Thaumaturgy\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} -\",\n        \"{@bold Range:} -\",\n        \"{@bold PE Cost:} Special\",\n        \"You may generate effects, as with the spell {@spell thaumaturgy}.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.featureType\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"featureType\": [\n        \"minT\"\n      ],\n      \"prerequisite\": [\n        {\n          \"feature\": [\n            \"Minor Talent\"\n          ],\n          \"psionics\": true\n        }\n      ]\n    },\n    {\n      \"name\": \"Psi Whisper\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} -\",\n        \"{@bold Range:} -\",\n        \"{@bold PE Cost:} Special\",\n        \"You may communicate with another as with the spell {@spell message}.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.featureType\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"featureType\": [\n        \"minT\"\n      ],\n      \"prerequisite\": [\n        {\n          \"feature\": [\n            \"Minor Talent\"\n          ],\n          \"psionics\": true\n        }\n      ]\n    },\n    {\n      \"name\": \"Mislead\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} -\",\n        \"{@bold Range:} -\",\n        \"{@bold PE Cost:} Special\",\n        \"You generate an illusion in the mind of another, as with the spell {@spell minor illusion}.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.featureType\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"featureType\": [\n        \"minT\"\n      ],\n      \"prerequisite\": [\n        {\n          \"feature\": [\n            \"Minor Talent\"\n          ],\n          \"psionics\": true\n        }\n      ]\n    },\n    {\n      \"name\": \"Animal Magnetism\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} Up to 1 minute\",\n        \"{@bold Range:} 5 ft\",\n        \"{@bold PE Cost:} Special\",\n        \"For up to 1 minute, you have advantage on all Charisma checks directed at one creature of your choice that isn't hostile toward you, as with the spell {@spell friends}.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.featureType\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"featureType\": [\n        \"minT\"\n      ],\n      \"prerequisite\": [\n        {\n          \"feature\": [\n            \"Minor Talent\"\n          ],\n          \"psionics\": true\n        }\n      ]\n    },\n    {\n      \"name\": \"Psi Bolt\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} Instantaneous\",\n        \"{@bold Range:} 120 feet\",\n        \"{@bold PE Cost:} Special\",\n        \"You hurl a bolt of force at a creature or object within range. Make a ranged psionic attack against the target. On a hit, the target takes 1d6 force damage.\",\n        \"This attack's damage increases by your psychic energy die at levels 5, 11, and 17.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"action\",\n        \"activation.cost\": \"1\",\n        \"range\": {\n          \"value\": 120,\n          \"units\": \"ft\"\n        },\n        \"duration\": {\n          \"value\": \"\",\n          \"units\": \"inst\"\n        },\n        \"actionType\": \"rsak\",\n        \"ability\": \"con\",\n        \"target.value\": 1,\n        \"target.type\": \"creature\",\n        \"type\": {\n          \"value\": \"class\",\n          \"subtype\": \"psionicPower\"\n        },\n        \"damage\": {\n          \"parts\": [\n            [\n              \"@scale.psion.talent-scale\",\n              \"force\"\n            ]\n          ]\n        }\n      },\n      \"featureType\": [\n        \"minT\"\n      ],\n      \"prerequisite\": [\n        {\n          \"feature\": [\n            \"Minor Talent\"\n          ],\n          \"psionics\": true\n        }\n      ],\n      \"foundryImg\": \"media/icons/psionics/psi-bolt.webp\"\n    },\n    {\n      \"name\": \"Scanning\",\n      \"source\": \"Psion\",\n      \"featureType\": [\n        \"AP\"\n      ],\n      \"prerequisite\": [\n        {\n          \"psionics\": true\n        }\n      ],\n      \"entries\": [\n        \"The scanning aptitude allows the psion to gain information from the world around them. Such information might include the emotions or thoughts of others, enhanced perception of the environment, or even allow one to experience the world through another's mind.\"\n      ]\n    },\n    {\n      \"name\": \"Sending\",\n      \"source\": \"Psion\",\n      \"prerequisite\": [\n        {\n          \"psionics\": true\n        }\n      ],\n      \"entries\": [\n        \"The sending aptitude allows the psion to project psychic energy or information they possess into the world around them. Such information might include emotions or thoughts, allowing communication or influence of a creature's mind, similarly the sending might alter a creature's perception of its environment. The sending aptitude also allows a Psion to engage in various kinds of mental attack, as they project their psyche into another's mind.\"\n      ],\n      \"featureType\": [\n        \"AP\"\n      ]\n    },\n    {\n      \"name\": \"Porting\",\n      \"source\": \"Psion\",\n      \"prerequisite\": [\n        {\n          \"psionics\": true\n        }\n      ],\n      \"entries\": [\n        \"The porting aptitude allows the psion to change their location or the location of others by warping space around them.\"\n      ],\n      \"featureType\": [\n        \"AP\"\n      ]\n    },\n    {\n      \"name\": \"Energizing\",\n      \"source\": \"Psion\",\n      \"featureType\": [\n        \"AP\"\n      ],\n      \"prerequisite\": [\n        {\n          \"psionics\": true\n        }\n      ],\n      \"entries\": [\n        \"The energizing aptitude concerns the ability to manipulate and harness various forms of energy, including psychic energy, elemental forces, and even life force. This can also be used to create energy shields, enhance physical abilities, or even resurrect the dead.\"\n      ]\n    },\n    {\n      \"name\": \"Nullifying\",\n      \"source\": \"Psion\",\n      \"featureType\": [\n        \"AP\"\n      ],\n      \"prerequisite\": [\n        {\n          \"psionics\": true\n        }\n      ],\n      \"entries\": [\n        \"The nullifying aptitude can be used to negate or disrupt the powers of other psionic or magical beings, blocking their abilities or otherwise rendering them powerless. This can also be used to dispel magical effects or prevent divination.\"\n      ]\n    },\n    {\n      \"name\": \"Altering\",\n      \"source\": \"Psion\",\n      \"featureType\": [\n        \"AP\"\n      ],\n      \"prerequisite\": [\n        {\n          \"psionics\": true\n        }\n      ],\n      \"entries\": [\n        \"The transmuting aptitude encompasses a broad range of talents all related to change. It is also often referred to as {@italic psychokinesis} or {@italic telekinesis}. You can use this aptitude to manipulate matter and energy at a fundamental level, altering the physical properties of objects or creatures. This can also be used to transmute materials, reshape objects, or even transform creatures.\"\n      ]\n    }\n  ],\n  \"psionic\": [\n    {\n      \"name\": \"Empathic Insight\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Scanning\",\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} Concentration\",\n        \"{@bold Range:} 60/15 ft radius\",\n        \"{@bold PE Cost:} Roll\",\n        \"Roll your PE die. As an action, you can empathically read a creature that is within 60 feet of you. You learn its most powerful surface-level emotion. For example, you might learn that a creature is excited, fearful, angry, sad, or content. Whenever you make a Charisma check to interact with a creature whose aura you have read, you gain a bonus to your roll equal to your Constitution modifier (minimum of +1).\",\n        \"You can also use this power to detect the presence of sentient creatures you can't see. Roll your PE die. As an action, you can search for creatures within 15 feet of you. Any barriers of rock, wood, or metal thicker than a few inches will block your ability. A creature that is not sentient, explicitly does not experience emotions, is hidden from psionics, divination magic, or having their mind read is immune to this feature.\"\n      ],\n      \"type\": \"T\",\n      \"order\": \"Scanning\"\n    },\n    {\n      \"name\": \"Mind Reading\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Scanning\",\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} Concentration\",\n        \"{@bold Range:} 120 ft radius\",\n        \"{@bold PE Cost:} Roll\",\n        \"Roll your PE die. You can use your telepathic abilities to read the thoughts of another creature. As an action, choose one creature you can perceive within 120 feet of you. That creature must make a Wisdom saving throw against your Psion spell save DC. On a failed save, you can read the surface thoughts of the creature for a number of minutes equal to the value of a roll of your PE die.\",\n        \"You can also use this power to detect the presence of intelligent creatures you can't see. As an action, you can search for creatures within 15 feet of you. Any barriers of rock, wood, or metal thicker than a few inches will block your ability. A creature that has an intelligence score lower than 3, does not possess any language, is hidden from psionics, divination magic, or otherwise having their mind read is immune to this feature.\"\n      ],\n      \"type\": \"T\",\n      \"order\": \"Scanning\"\n    },\n    {\n      \"name\": \"Psychometry\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Scanning\",\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} Concentration\",\n        \"{@bold Range:} Touch\",\n        \"{@bold PE Cost:} Roll\",\n        \"Roll your PE die. You can make relevant inferences from an object of unknown history by making physical contact with that object. When used as an action, you gain a bonus equal to your PE die value on Investigation or History rolls with respect to an object you can touch. If you succeed on your skill check, you can gain specific information about the identity, appearance, or mental state of the creature or creatures that most recently handled the item or were most significantly related to its creation or history.\"\n      ],\n      \"type\": \"T\",\n      \"order\": \"Scanning\"\n    },\n    {\n      \"name\": \"Amodal Cognition\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Scanning\",\n        \"{@bold Type:} Bonus Action\",\n        \"{@bold Duration:} Concentration\",\n        \"{@bold Range:} 10 ft radius\",\n        \"{@bold PE Cost:} Roll/Size\",\n        \"Roll your PE die. You gain information about your environment without it coming to you via any sense modality. Instead, you simply \\\"know\\\" various facts concerning your location or the creatures occupying it. Whenever you make an Insight or Investigation check, you gain a bonus to your roll equal to your PE die roll value. This applies only to a location, item, or creature within 10 feet of you. To extend the range of your cognition, you must use an action and expend one PE die size per 10 feet of range. The extended range lasts until you lose concentration. Known information can include the name, owner, location, alignment, emotional valence, and even (surface level) thoughts of objects or creatures in range.\"\n      ],\n      \"type\": \"T\",\n      \"order\": \"Scanning\",\n      \"foundrySystem\": {\n        \"type\": {\n          \"value\": \"class\",\n          \"subtype\": \"psionicPower\"\n        }\n      }\n    },\n    {\n      \"name\": \"Third Eye\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Scanning\",\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} Concentration\",\n        \"{@bold Range:} 10 ft radius per PE die\",\n        \"{@bold PE Cost:} Roll/Size\",\n        \"You see as if through a third eye, which grants you enhanced sight in a 10-foot radius with you at the center. Within this range, you have blindsight and can see into the ethereal plane. While seeing through your third eye, you also gain proficiency in Perception, and whenever you make a Wisdom (Perception) check, you gain a bonus to your roll equal to your Constitution modifier (minimum of +1).\",\n        \"As a bonus action, you can expend a PE die size and expand the radius of your enhanced sight by 10 feet per PE die size expended.\"\n      ],\n      \"type\": \"T\",\n      \"order\": \"Scanning\"\n    },\n    {\n      \"name\": \"Remote Viewing\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Scanning\",\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} 1 min per PE die; requires {@italic concentration}\",\n        \"{@bold Range:} PE die value in miles\",\n        \"{@bold PE Cost:} Roll/Size\",\n        \"You can see and hear events occurring at a location within range as if you were there, even if you have never been to that location before. As an action, you can choose a location that you know exists and is within a number of miles equal to your PE die size. For the next minute, you can see and hear everything happening at that location, as if you were standing there yourself. You can extend the duration by expending a die size. You can move your point of view around the location as if you were physically present. However, you cannot interact with anything at the location or communicate with any creatures there. You must concentrate to maintain this ability, and you cannot move or take other actions while using it. If you use this more than once per long rest, you must expend a PE die size for every additional use until you take a long rest.\"\n      ],\n      \"type\": \"T\",\n      \"order\": \"Scanning\"\n    },\n    {\n      \"name\": \"Precognition\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Scanning\",\n        \"{@bold Type:} Bonus Action\",\n        \"{@bold Duration:} 1 minute\",\n        \"{@bold Range:} Self\",\n        \"{@bold PE Cost:} Roll/Size\",\n        \"You gain a premonition of future events. As an action, you can roll your PE die and add the result to an attack roll, ability check, or saving throw you make within the next minute. You can use this talent once per long rest without expending any PE die size. If you use this more than once per long rest, you must expend a PE die size for every use until you take a long rest.\"\n      ],\n      \"type\": \"T\",\n      \"order\": \"Scanning\"\n    },\n    {\n      \"name\": \"Retrocognition\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Scanning\",\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} Concentration\",\n        \"{@bold Range:} 30 ft radius\",\n        \"{@bold PE Cost:} Roll\",\n        \"You can sense information about past events that occurred in your current location or nearby. As an action, you can roll your PE die and learn information about an event that took place within the area you can see or touch, up to a number of years equal to the number rolled. The information you receive is based on the nature of the event and the degree of its significance. For example, you might learn the identity of a person who was recently in the area, the location of a hidden object, or the details of a past battle. You cannot use this talent to gain information about events that occurred beyond the range of your senses or that you have no way of physically accessing. You may use this talent no more than once in a given area before taking a short or long rest.\"\n      ],\n      \"type\": \"T\",\n      \"order\": \"Scanning\"\n    },\n    {\n      \"name\": \"Communion\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Sending\",\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} 1 hour per PE die size expended; concentration\",\n        \"{@bold Range:} Perceptual range, no longer required after establishment\",\n        \"{@bold PE Cost:} Size\",\n        \"You may communicate your emotions or thoughts to the mind of another intelligent being that you can immediately perceive. The recipient recognizes these emotions or thoughts as not their own, and may make an Intelligence check to determine their source. This talent effectively allows one-way communication between sender and recipient. The communion lasts 1 hour per PE die size expended. After the communion is established, the recipient need no longer be within immediate perceptual range, and communication is in all respects similar to talking with the creature.\",\n        \"Any barriers of rock, wood, or metal thicker than a few inches will block your ability. A creature that has an intelligence score lower than 3, does not possess any language, is hidden from psionics or divination magic is immune to this feature.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"type\": \"T\",\n      \"order\": \"Sending\"\n    },\n    {\n      \"name\": \"Thought Intrusion\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Sending\",\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} Instantaneous\",\n        \"{@bold Range:} 60 feet\",\n        \"{@bold PE Cost:} Roll\",\n        \"As an action, you can use your psychic energy to send intrusive thoughts to a creature within 60 feet of you that you can see or otherwise know the exact location of. The target must make a Wisdom saving throw against your psion DC or take 1d6 + PE die roll of psychic damage and become frightened for a number of rounds equal to the PE roll (minimum of 1). On a successful save, the creature takes half damage and is not frightened.\",\n        \"Any barriers of rock, wood, or metal thicker than a few inches will block your ability. A creature that has an intelligence score lower than 3, does not possess any language, or is hidden from psionics or divination magic is immune to this feature.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"type\": \"T\",\n      \"order\": \"Sending\"\n    },\n    {\n      \"name\": \"Mind Link\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Sending, and either Communion or Mind Reading\",\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} Roll hours\",\n        \"{@bold Range:} 1 mile\",\n        \"{@bold PE Cost:} Roll\",\n        \"You can establish telepathic communication between yourself and others. As an action, choose one or more creatures you can see, up to a number of creatures equal to twice your proficiency bonus, and then roll your Psionic Energy die. For a number of hours equal to the number rolled, the chosen creatures can speak telepathically with you, and you can speak telepathically with them. To send or receive a message (no action required), you and the other creature must be within 1 mile of each other. A creature can't use this telepathy if it can't speak any languages, and a creature can end the telepathic connection at any time (no action required). You and the creature don't need to speak a common language to understand each other.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"type\": \"T\",\n      \"order\": \"Sending\"\n    },\n    {\n      \"name\": \"Mind Hammer\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Sending\",\n        \"{@bold Type:} {@action Attack}\",\n        \"{@bold Duration:} Instantaneous\",\n        \"{@bold Range:} 60 feet\",\n        \"{@bold PE Cost:} Roll\",\n        \"As an action, choose one creature you can see within 60 feet of you. The target must make an Intelligence saving throw. On a failed save, it takes 1d10 psychic damage per PE die spent and suffers disadvantage on its next Wisdom saving throw before the end of your next turn. On a successful save, it takes half as much damage and suffers no disadvantage.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"type\": \"T\",\n      \"order\": \"Sending\"\n    },\n    {\n      \"name\": \"Mind Blast\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Sending\",\n        \"{@bold Type:} {@action Attack}\",\n        \"{@bold Duration:} Instantaneous\",\n        \"{@bold Range:} 30-foot cone\",\n        \"{@bold PE Cost:} 2 Sizes\",\n        \"As an action, you expend two PE die sizes to project a blast of psychic energy in a 30-foot cone in front of you. Each creature in the cone must make a Wisdom saving throw or take (3d8 + Consitution modifier) of psychic damage and be stunned for 1 round. On a successful save, the creature takes half damage and is not stunned. Add 1d8 of additional psychic damage at levels 5, 12, and 18.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"type\": \"T\",\n      \"order\": \"Sending\"\n    },\n    {\n      \"name\": \"Mind Meld\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Sending\",\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} Instantaneous\",\n        \"{@bold Range:} Touch\",\n        \"{@bold PE Cost:} Roll\",\n        \"As an action, you link your mind with that of another humanoid you can touch. The target must succeed on a Wisdom saving throw against your Psionics save DC or become charmed by you for one hour or until it takes damage. If the save is successful the target is immune to this talent for 24 hours. After the duration ends the target will know that is has been charmed. While the target is charmed in this way, you can communicate with it telepathically. You may use this talent once per target before requiring a long rest. You can mind-meld with a number of humanoids equal to proficiency bonus at a time.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"type\": \"T\",\n      \"order\": \"Sending\"\n    },\n    {\n      \"name\": \"Psychic Command\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Sending\",\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} 1 minute\",\n        \"{@bold Range:} 60 feet\",\n        \"{@bold PE Cost:} Roll\",\n        \"As an action, you attempt to charm a creature you can see within 60 feet of you. The target creature must succeed on a Wisdom saving throw against your Psionics save DC or be charmed by you for 1 minute. While charmed in this way, the creature is under your control, and you can issue it simple commands, such as \\\"Approach,\\\" \\\"Flee,\\\" \\\"Halt,\\\" or \\\"Drop what you're holding.\\\" The creature will not obey any commands that might cause it harm. Once the effect ends, the creature becomes aware of your control and can take actions against you if it wishes.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"type\": \"T\",\n      \"order\": \"Sending\"\n    },\n    {\n      \"name\": \"Psionic Disguise\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Sending\",\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} 1 hour\",\n        \"{@bold Range:} Self\",\n        \"{@bold PE Cost:} Roll\",\n        \"You can use your psionic power to alter your appearance. As an action, you can alter your physical appearance in any way you choose, as if you had cast the {@spell disguise self|phb} spell. The effect lasts for up to 1 hour or until you dismiss it as a bonus action. Additional uses of this talent prior to taking a long rest require expending a PE die size.\"\n      ],\n      \"type\": \"T\",\n      \"order\": \"Sending\"\n    },\n    {\n      \"name\": \"Mental Insinuation\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Sending\",\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} Concentration\",\n        \"{@bold Range:} 30 ft\",\n        \"{@bold PE Cost:} Roll\",\n        \"You subtly insinuate your preferences to another and thus influence their choices without ever overtly making your influence noticed. While concentrating on this talent, you gain expertise in the Persuasion skill against a creature you can perceive within 30 feet of you, though the talent has no effect against creatures that are immune to psionics, or immune to mental influence or charm (such as a creature under the effect of mind blank).\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"type\": \"T\",\n      \"order\": \"Sending\"\n    },\n    {\n      \"name\": \"Psychic Presence\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Sending\",\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} 1 minute\",\n        \"{@bold Range:} 30 feet\",\n        \"{@bold PE Cost:} Roll\",\n        \"You project a psionic aura that can either draw or repel attention from others. As an action, choose one creature you can see within 30 feet of you. That creature must make a Wisdom saving throw against your Psionic save DC or be drawn to you for the next minute, as if by a {@spell charm person|phb} spell\",\n        \"Alternatively, you can choose to repel the creature, causing it to make an immediate Wisdom saving throw against your Psionic save DC or be unable to approach within 30 feet of you for the next minute.\",\n        \"Additional uses of this talent prior to taking a long rest require expending a PE die size.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"type\": \"T\",\n      \"order\": \"Sending\"\n    },\n    {\n      \"name\": \"Mental Image\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Sending\",\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} 10 minutes (Concentration)\",\n        \"{@bold Range:} 60 feet\",\n        \"{@bold PE Cost:} Size\",\n        \"You gain the ability to project images into the minds of creatures with perfect clarity, interposing it over their reality. Creatures affected by this talent must make an Intelligence saving throw or be convinced that the image is real, even if it contradicts other sensory input. The image lasts for up to 10 minutes, until your concentration is interrupted, or until you choose to end it, whichever comes first. You can target up to three creatures with this talent at once and must have a clear line of sight to them.\",\n        \"A creature that has an intelligence score lower than 3, does not possess any language, or is hidden from psionics or divination magic is immune to this feature.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"type\": \"T\",\n      \"order\": \"Sending\"\n    },\n    {\n      \"name\": \"Mental Shadow\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Sending\",\n        \"{@bold Type:} Bonus Action\",\n        \"{@bold Duration:} 1 minute\",\n        \"{@bold Range:} Touch\",\n        \"{@bold PE Cost:} Roll\",\n        \"You blend your own presence and memories with that of another creature, causing it to perceive you as a part of its own mind. As a bonus action, you merge with the mind of a creature you touch. While merged, you can perceive the creature's surroundings as if you were there and can communicate telepathically with the creature. You have advantage on Charisma (Persuasion) checks made to influence the creature while merged. The creature is unaware that you are influencing its thoughts unless you choose to reveal yourself or your actions would make it obvious. This talent lasts for 1 minute, until your concentration is interrupted, or until you choose to end it as a bonus action. You may extend the duration by expending a PE die. Once you use this talent, you can't use it again on the same creature until you complete a long rest.\"\n      ],\n      \"type\": \"T\",\n      \"order\": \"Sending\"\n    },\n    {\n      \"name\": \"Spatial Swap\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Porting\",\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} Instantaneous\",\n        \"{@bold Range:} 30 feet\",\n        \"{@bold PE Cost:} Size\",\n        \"You can swap the positions of two creatures or objects within 30 feet of you that you can see. The swap must place each target in a location that it can occupy and cannot move the target into a hazardous location, such as the middle of a wall or a pool of lava.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"type\": \"T\",\n      \"order\": \"Porting\"\n    },\n    {\n      \"name\": \"Blink\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Porting\",\n        \"{@bold Type:} Bonus Action\",\n        \"{@bold Duration:} Instantaneous\",\n        \"{@bold Range:} Self\",\n        \"{@bold PE Cost:} Roll\",\n        \"As a bonus action, you may teleport up to the PE die value * 5 feet to an unoccupied space you can see. Prior to a short or long rest, for each use of Blink after the first, whatever the PE die value rolled, you must expend a PE die size. You do not expend more than one die size per Blink.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"bonus\",\n        \"activation.cost\": \"1\"\n      },\n      \"type\": \"T\",\n      \"order\": \"Porting\"\n    },\n    {\n      \"name\": \"Portal\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Porting\",\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} 1 minute\",\n        \"{@bold Range:} 60 feet\",\n        \"{@bold PE Cost:} Size\",\n        \"You create a portal between two points that you can sense that are within 60 feet of you. The portal is a circular opening that is 5 feet in diameter, and it lasts for up to 1 minute, unless it is dismissed, or your concentration is interrupted. Any creature or object that enters the portal from either side is immediately transported to the other side of the portal, appearing in an unoccupied space that you can see within 5 feet of the portal. The portal can be used up to three times before it disappears. The portal cannot be used offensively.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"type\": \"T\",\n      \"order\": \"Porting\"\n    },\n    {\n      \"name\": \"Teleport\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Porting\",\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} Instantaneous\",\n        \"{@bold Range:} 30 feet\",\n        \"{@bold PE Cost:} Size\",\n        \"You teleport yourself, and any creatures or objects you choose of up to large size within 30 feet of you, to an unoccupied space you can see within 500 feet. You arrive at exactly the spot desired. It can be a place you can see, one you can visualize, or one you can describe by stating distance and direction, such as \\\"200 feet straight downward\\\" or \\\"upward to the northwest at a 45-degree angle, 300 feet.\\\" You may do this once per long rest. If you teleport a number of creatures (or objects of medium or larger size) greater than your proficiency bonus, you must take a level of exhaustion per extra creature/object transported.\",\n        \"For each use of Teleport after the first you must expend a PE die size until you take a long rest.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"type\": \"T\",\n      \"order\": \"Porting\"\n    },\n    {\n      \"name\": \"Shaping\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Altering\",\n        \"{@bold Type:} {@action Use an object}\",\n        \"{@bold Duration:} Until long rest or reshaped again\",\n        \"{@bold Range:} Touch\",\n        \"{@bold PE Cost:} Roll\",\n        \"You can use this talent to reshape non-living material objects, manipulating them to take on different forms or shapes. You can reshape an object with a volume of up to 1 cubic foot per psychic energy die expended, using a minimum of one die. You cannot alter the nature of the object (e.g., you cannot make stone turn into clay), but you can reshape the object beyond what it would normally allow (e.g., you can curve a stone or piece of wood that would otherwise break).\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"type\": \"T\",\n      \"order\": \"Altering\"\n    },\n    {\n      \"name\": \"Fusion\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Altering\",\n        \"{@bold Type:} {@action Use an object}\",\n        \"{@bold Duration:} Until short rest or unfused\",\n        \"{@bold Range:} Touch\",\n        \"{@bold PE Cost:} Size\",\n        \"You can use this talent to fuse two objects or substances together into a single, unified whole. The objects or substances being fused must be compatible with each other (e.g., you cannot fuse water and iron or plasma and flesh). The total mass of the objects or substances cannot exceed 10 pounds per psychic energy die expended, using a minimum of one die.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"type\": \"T\",\n      \"order\": \"Altering\"\n    },\n    {\n      \"name\": \"Telekinetic Strike\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Altering\",\n        \"{@bold Type:} {@action Attack}\",\n        \"{@bold Duration:} Instantaneous\",\n        \"{@bold Range:} 50 feet\",\n        \"{@bold PE Cost:} Roll/Size\",\n        \"You focus your psionic energy into a powerful blow to an enemy within 50 feet of you. The attack does 2d6 + PE die. Additional uses of this talent prior to taking a short or long rest require expending a PE die size per use.\",\n        \"The attack damage increases an additional 1d6 at levels 5, 9, 13, and 17.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"type\": \"T\",\n      \"order\": \"Altering\"\n    },\n    {\n      \"name\": \"Elemental Shift\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Altering\",\n        \"{@bold Type:} {@action Use an object}\",\n        \"{@bold Duration:} Instantaneous\",\n        \"{@bold Range:} 30 feet\",\n        \"{@bold PE Cost:} Size\",\n        \"You alter the elemental composition of a non-magical, non-living object of up to medium size within 30 feet of you, transmuting it into a different substance of equal or lesser value. The weight of the object cannot exceed 10 pounds per psychic energy die expended, using a minimum of one die.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"type\": \"T\",\n      \"order\": \"Altering\"\n    },\n    {\n      \"name\": \"Density Control\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Altering\",\n        \"{@bold Type:} {@action Use an object}\",\n        \"{@bold Duration:} Instantaneous\",\n        \"{@bold Range:} 30 feet\",\n        \"{@bold PE Cost:} Roll\",\n        \"You manipulate the density of a non-magical object or creature within 30 feet of you. Roll your Psionic Energy die and add the result to an ability check, attack roll, or saving throw made against the target. Additional uses of this talent prior to taking a short or long rest require expending a PE die size per use.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"type\": \"T\",\n      \"order\": \"Altering\"\n    },\n    {\n      \"name\": \"Pyro/Cryokinesis\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Altering\",\n        \"{@bold Type:} {@action Use an object} or {@action Attack}\",\n        \"{@bold Duration:} 1 round\",\n        \"{@bold Range:} 15 feet\",\n        \"{@bold PE Cost:} Roll\",\n        \"You can use psionic energy to heat up or cool down a perceivable creature or object within 15 feet of you. If a creature is targeted, it must make a Wisdom saving throw against your Psionics DC or take {@damage 1d10} plus your PE die roll in heat/cold damage at the beginning of its turn until the Psion ends the effect or their concentration is broken.\",\n        \"If an object is targeted then it takes similar damage, and each round it is concentrated upon there is a cumulative 25% chance of it cracking (cold) or igniting (heat). If the material is metal that is being worn the effect is similar to that of {@spell heat metal}.\",\n        \"Additional uses of this talent prior to taking a short or long rest require expending a PE die size per use.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"type\": \"T\",\n      \"order\": \"Altering\"\n    },\n    {\n      \"name\": \"Cryo/Pyrokinetic Blast\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Altering\",\n        \"{@bold Type:} {@action Attack}\",\n        \"{@bold Duration:} Instantaneous\",\n        \"{@bold Range:} 60 feet\",\n        \"{@bold PE Cost:} Roll\",\n        \"You unleash a blast of intense heat/cold from your mind, dealing fire/cold damage to a target within 30 feet of you. Make a ranged psionic attack against the target. If the attack hits, the creature takes {@damage 3d8} damage plus your PE die value of the type you chose.\",\n        \"The damage increases by 1d8 for each PE die size spent \"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"type\": \"T\",\n      \"order\": \"Altering\"\n    },\n    {\n      \"name\": \"Telekinetic Barrier\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Altering\",\n        \"{@bold Type:} Reaction\",\n        \"{@bold Duration:} 1 minute\",\n        \"{@bold Range:} Self\",\n        \"{@bold PE Cost:} Roll\",\n        \"You create a barrier of telekinetic energy to protect yourself from harm. Roll your Psionic Energy die. The barrier has 2 hit points for every point rolled on the die. The barrier lasts for one minute or until it has been depleted. Each subsequent use of this talent prior to a long rest requires expending a PE die size.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"reaction\",\n        \"activation.cost\": \"1\",\n        \"activation.condition\": \"when you are attacked.\"\n      },\n      \"type\": \"T\",\n      \"order\": \"Altering\"\n    },\n    {\n      \"name\": \"Power Nullification\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Nullifying\",\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} 1 minute\",\n        \"{@bold Range:} 10 ft radius\",\n        \"{@bold PE Cost:} Roll\",\n        \"You can use this talent to completely suppress the use of psionic or magical powers within a 10 ft radius around you. This suppression lasts for 1 minute, and you must concentrate on the suppression throughout.\",\n        \"Each subsequent use of this talent prior to a long rest requires expending a PE die size.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"type\": \"T\",\n      \"order\": \"Nullifying\"\n    },\n    {\n      \"name\": \"Dispel Magic\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Nullifying\",\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} 1 round\",\n        \"{@bold Range:} 50 ft\",\n        \"{@bold PE Cost:} Size\",\n        \"You can use this talent to dispel magical effects within a 50 ft range. This requires a concentration time of 1 round, after which the magic is dispelled.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"type\": \"T\",\n      \"order\": \"Nullifying\"\n    },\n    {\n      \"name\": \"Counterpsionics\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Nullifying\",\n        \"{@bold Type:} Reaction\",\n        \"{@bold Duration:} Instantaneous\",\n        \"{@bold Range:} 30 ft\",\n        \"{@bold PE Cost:} Special\",\n        \"When a psionic effect is used within 30 ft of you, you can use your reaction to attempt to counteract it. This requires a successful Intelligence (Psionics) check, contested by the original caster's Intelligence (Psionics) check. On a successful contest, the effect is nullified.\",\n        \"Each subsequent use of this talent prior to a short or long rest requires expending a PE die size.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"reaction\",\n        \"activation.cost\": \"1\"\n      },\n      \"type\": \"T\",\n      \"order\": \"Nullifying\"\n    },\n    {\n      \"name\": \"Energy Drain\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Nullifying\",\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} Instantaneous\",\n        \"{@bold Range:} 60 ft\",\n        \"{@bold PE Cost:} Roll\",\n        \"You can use this talent to drain the psionic or magical energy of a creature within 60 ft of you. This requires a successful Intelligence (Psionics) check, contested by the target's Wisdom saving throw. On a failed save, the creature takes 2d10 psychic damage and loses one spell slot or PE die size.\",\n        \"Each subsequent use of this talent prior to a long rest requires expending a PE die size.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"type\": \"T\",\n      \"order\": \"Nullifying\"\n    },\n    {\n      \"name\": \"Anti-divination\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Nullifying\",\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} 1 minute\",\n        \"{@bold Range:} 30 ft radius\",\n        \"{@bold PE Cost:} Roll\",\n        \"You can use this talent to disrupt the use of divination magic within a 30 ft radius around you. For 1 minute, any attempt to scry or gain information about the area or creatures within the area must overcome a DC equal to 8 + your Constitution modifier + your proficiency bonus.\",\n        \"Each subsequent use of this talent prior to a short or long rest requires expending a PE die size.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"type\": \"T\",\n      \"order\": \"Nullifying\"\n    },\n    {\n      \"name\": \"Energy Shield\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Energizing\",\n        \"{@bold Type:} Reaction\",\n        \"{@bold Duration:} 1 minute\",\n        \"{@bold Range:} 30 feet\",\n        \"{@bold PE Cost:} Size\",\n        \"You can create a protective shield made of energy around yourself or a creature within 30 feet of you. The shield grants a bonus to AC equal to your proficiency bonus and lasts for 1 minute. The cost to use this talent is expending a psychic energy die size.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"reaction\",\n        \"activation.cost\": \"1\"\n      },\n      \"type\": \"T\",\n      \"order\": \"Energizing\"\n    },\n    {\n      \"name\": \"Vitality Surge\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Energizing\",\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} Instantaneous\",\n        \"{@bold Range:} 30 feet\",\n        \"{@bold PE Cost:} Size\",\n        \"You can channel energy into a creature, restoring a number of hit points equal to your PE die size plus your proficiency bonus. The target must be within 30 feet of you and must be a living creature. The cost to use this talent is expending a psychic energy die size.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"type\": \"T\",\n      \"order\": \"Energizing\"\n    },\n    {\n      \"name\": \"Elemental Resistance\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Energizing\",\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} 1 hour\",\n        \"{@bold Range:} touch\",\n        \"{@bold PE Cost:} Size\",\n        \"You can imbue yourself or a creature you can touch with resistance to a chosen elemental damage type (acid, cold, fire, lightning, poison, or thunder) for 1 hour. The cost to use this talent is expending a psychic energy die size.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"type\": \"T\",\n      \"order\": \"Energizing\"\n    },\n    {\n      \"name\": \"Life Transference\",\n      \"source\": \"Psion\",\n      \"entries\": [\n        \"{@bold Prerequisite:} Energizing\",\n        \"{@bold Type:} Action\",\n        \"{@bold Duration:} Instantaneous\",\n        \"{@bold Range:} touch\",\n        \"{@bold PE Cost:} Roll\",\n        \"You can sacrifice some of your own life energy to restore the vital energy of another creature you can touch. You take damage equal to your psychic energy die roll minus your Constitution modifier (minimum of 1 damage), while the target regains hit points equal to your PE die {@i size}. The target creature must be a living creature.\"\n      ],\n      \"foundrySystem\": {\n        \"activation.type\": \"action\",\n        \"activation.cost\": \"1\"\n      },\n      \"type\": \"T\",\n      \"order\": \"Energizing\"\n    }\n  ]\n}\n"
  },
  {
    "path": "src/test/resources/5e/sample.yaml",
    "content": "sources:\n  adventure:\n    - CM\n    - DC\n    - DIP\n    - FS\n    - GoS\n    - IDRotF\n    - LMoP\n    - LOX\n    - OoW\n    - PotA\n    - SDW\n    - SLW\n    - TftYP-AtG\n    - TftYP-DiT\n    - TftYP-TFoF\n    - TftYP-THSoT\n    - TftYP-TSC\n    - TftYP-ToH\n    - TftYP-WPM\n    - WBtW\n    - WDH\n    - WDMM\n  book:\n    - AAG\n    - AI\n    - BAM\n    - DoD\n    - EGW\n    - FTD\n    - MaBJoV\n    - MM\n    - MPMM\n    - MTF\n    - PHB\n    - SCAG\n    - SCC\n    - TCE\n    - VGM\n    - XGE\n  reference:\n    - AWM\n    - DMG\n    - EEPC\n    - ESK\n    - TftYP\n    - SaF\n  homebrew:\n    - sources/5e-homebrew/collection/MCDM Productions; Strongholds and Followers.json\n    - sources/5e-homebrew/collection/Darrington Press; Tal’Dorei Campaign Setting Reborn.json\n\npaths:\n  compendium: /compendium/5e/\n  rules: /compendium/5e/rules/\n\ninclude:\n  - race|genasi|eepc\n  - racefluff|genasi|eepc\n  - subrace|air|genasi|eepc\n  - subrace|earth|genasi|eepc\n  - subrace|fire|genasi|eepc\n  - subrace|water|genasi|eepc\n\nexcludePattern:\n  - race\\|.*\\|dmg\n\nexclude:\n  - monster|expert|dc\n  - monster|expert|sdw\n  - monster|expert|slw\n\ntemplate:\n  background: examples/templates/tools5e/mixed/mixed-background2md.txt\n  class: examples/templates/tools5e/mixed/mixed-class2md.txt\n  deity: examples/templates/tools5e/mixed/mixed-deity2md.txt\n  feat: examples/templates/tools5e/mixed/mixed-feat2md.txt\n  hazard: examples/templates/tools5e/mixed/mixed-hazard2md.txt\n  item: examples/templates/tools5e/mixed/mixed-item2md.txt\n  monster: examples/templates/tools5e/mixed/mixed-monster2md.txt\n  object: examples/templates/tools5e/mixed/mixed-object2md.txt\n  race: examples/templates/tools5e/mixed/mixed-race2md.txt\n  reward: examples/templates/tools5e/mixed/mixed-reward2md.txt\n  spell: examples/templates/tools5e/mixed/mixed-spell2md.txt\n  subclass: examples/templates/tools5e/mixed/mixed-subclass2md.txt\n  vehicle: examples/templates/tools5e/mixed/mixed-vehicle2md.txt\n\nimages:\n  copyInternal: true\n  internalRoot: sources/5etools-img\n\nuseDiceRoller: true\nyamlStatblocks: false\nonlyReferencedTables: true\n"
  },
  {
    "path": "src/test/resources/5e/sources-2014-book-adventure.json",
    "content": "{\n  \"paths\": {\n    \"rules\": \"/ru les/\",\n    \"compendium\": \"/compend ium/\"\n  },\n  \"sources\": {\n    \"adventure\": [\n      \"WBtW\",\n      \"MOT-NSS\",\n      \"DIP\",\n      \"\"\n    ],\n    \"book\": [\n      \"PHB\",\n      \"MOT\",\n      \"\"\n    ],\n    \"reference\": [\n      \"DMG\",\n      \"XGE\",\n      \"TCE\",\n      \"\"\n    ],\n    \"homebrew\": [\n      \"\"\n    ]\n  },\n  \"include\": [\n    \"race|changeling|mpmm\"\n  ],\n  \"exclude\": [\n    \"monster|expert|dc\",\n    \"monster|expert|sdw\",\n    \"monster|expert|slw\"\n  ],\n  \"excludePattern\": [\n    \"race\\\\|.*\\\\|dmg\"\n  ]\n}\n"
  },
  {
    "path": "src/test/resources/5e/sources-2014-no-phb.yaml",
    "content": "full-source:\n  adventure:\n  - WDH\n  - LMoP\n  book:\n  - VGM\n"
  },
  {
    "path": "src/test/resources/5e/sources-2014-srd.yaml",
    "content": "sources:\n  reference:\n    - srd\n    - basicrules\n"
  },
  {
    "path": "src/test/resources/5e/sources-2014-subset.json",
    "content": "{\n  \"from\": [\n    \"PHB\",\"DMG\",\"XGE\",\"SCAG\"\n  ]\n}\n"
  },
  {
    "path": "src/test/resources/5e/sources-2024-srd.yaml",
    "content": "sources:\n  reference:\n  - \"srd52\"\n  - \"basicRules2024\"\nracesAsSpecies: true\nsplitRules: true\n"
  },
  {
    "path": "src/test/resources/5e/sources-2024-subset.yaml",
    "content": "sources:\n  adventure:\n    - DIP\n  reference:\n    - XMM\n    - XPHB\n    - XDMG\n"
  },
  {
    "path": "src/test/resources/5e/sources-changeDefaultSources.yaml",
    "content": "sources:\n  reference:\n    - phb\n    - xphb\n    - mm\n    - xmm\n    - dmg\n    - xdmg\n  defaultSource:\n    background: xphb\n    classtype: xphb\n    feat: xphb\n    optfeature: xphb\n    race: xphb\n    subrace: xphb\n    spell: xphb\n\n    deck: xdmg\n\n    item: xdmg\n    object: xdmg\n    reward: xdmg\n    table: xdmg\n    tableGroup: xdmg\n    variantrule: dmg\n\n    monster: xmm\n\n\n"
  },
  {
    "path": "src/test/resources/5e/sources-homebrew.json",
    "content": "{\n  \"sources\": {\n    \"adventure\": [\n      \"LMOP\"\n    ],\n    \"book\": [\n      \"PSA\"\n    ],\n    \"homebrew\": [\n      \"src/test/resources/5e/psion.json\",\n      \"src/test/resources/5e/ermis-bg.json\",\n      \"sources/5e-homebrew/adventure/Anthony Joyce; The Blood Hunter Adventure.json\",\n      \"sources/5e-homebrew/adventure/Arcanum Worlds; Odyssey of the Dragonlords.json\",\n      \"sources/5e-homebrew/adventure/JVC Parry; Call from the Deep.json\",\n      \"sources/5e-homebrew/adventure/Kobold Press; Book of Lairs.json\",\n      \"sources/5e-homebrew/background/D&D Wiki; Featured Quality Backgrounds.json\",\n      \"sources/5e-homebrew/book/Ghostfire Gaming; Grim Hollow Campaign Guide.json\",\n      \"sources/5e-homebrew/book/Ghostfire Gaming; Stibbles Codex of Companions.json\",\n      \"sources/5e-homebrew/book/MCDM Productions; Arcadia Issue 3.json\",\n      \"sources/5e-homebrew/class/D&D Wiki; Swashbuckler.json\",\n      \"sources/5e-homebrew/class/Foxfire94; Vampire.json\",\n      \"sources/5e-homebrew/class/KibblesTasty; Inventor.json\",\n      \"sources/5e-homebrew/class/Matthew Mercer; Blood Hunter (2022).json\",\n      \"sources/5e-homebrew/class/badooga; Badooga's Psion.json\",\n      \"sources/5e-homebrew/collection/Arcana Games; Arkadia.json\",\n      \"sources/5e-homebrew/collection/Darrington Press; Tal’Dorei Campaign Setting Reborn.json\",\n      \"sources/5e-homebrew/collection/Ghostfire Gaming; Grim Hollow - The Monster Grimoire.json\",\n      \"sources/5e-homebrew/collection/Jasmine Yang; Hamund's Herbalism Handbook.json\",\n      \"sources/5e-homebrew/collection/Keith Baker; Exploring Eberron.json\",\n      \"sources/5e-homebrew/collection/Kobold Press; Deep Magic 14 Elemental Magic.json\",\n      \"sources/5e-homebrew/collection/Kobold Press; Deep Magic.json\",\n      \"sources/5e-homebrew/collection/Loot Tavern; Heliana's Guide To Monster Hunting.json\",\n      \"sources/5e-homebrew/collection/MCDM Productions; The Talent and Psionics Open Playtest Round 2.json\",\n      \"sources/5e-homebrew/collection/Mage Hand Press; Valda's Spire of Secrets.json\",\n      \"sources/5e-homebrew/creature/Dragonix; Monster Manual Expanded III.json\",\n      \"sources/5e-homebrew/creature/Kobold Press; Creature Codex.json\",\n      \"sources/5e-homebrew/creature/Kobold Press; Tome of Beasts 2.json\",\n      \"sources/5e-homebrew/creature/Kobold Press; Tome of Beasts.json\",\n      \"sources/5e-homebrew/creature/MCDM Productions; Flee, Mortals! preview.json\",\n      \"sources/5e-homebrew/creature/MCDM Productions; Flee, Mortals!.json\",\n      \"sources/5e-homebrew/creature/Nerzugal Role-Playing; Nerzugal's Extended Bestiary.json\",\n      \"sources/5e-homebrew/deity/Frog God Games; The Lost Lands.json\",\n      \"sources/5e-homebrew/race/Middle Finger of Vecna; Archon.json\",\n      \"sources/5e-homebrew/spell/LaserLlama; LaserLlama's Compendium of Spells.json\",\n      \"sources/5e-homebrew/subclass/LaserLlama; Druid Circles.json\"\n    ],\n    \"reference\": [\n      \"DMG\",\n      \"MM\",\n      \"EGW\",\n      \"FTD\",\n      \"PHB\",\n      \"XPHB\"\n    ]\n  },\n  \"include\": [\n    \"race|changeling|mpmm\"\n  ],\n  \"exclude\": [\n    \"monster|expert|dc\",\n    \"monster|expert|sdw\",\n    \"monster|expert|slw\"\n  ],\n  \"excludePattern\": [\n    \"race\\\\|.*\\\\|dmg\"\n  ],\n  \"template\": {\n    \"background\": \"examples/templates/tools5e/images-background2md.txt\",\n    \"item\": \"examples/templates/tools5e/images-item2md.txt\",\n    \"monster\": \"src/test/resources/other/monster-all.txt\",\n    \"object\": \"examples/templates/tools5e/images-object2md.txt\",\n    \"race\": \"examples/templates/tools5e/images-race2md.txt\",\n    \"spell\": \"examples/templates/tools5e/images-spell2md.txt\",\n    \"vehicle\": \"examples/templates/tools5e/images-vehicle2md.txt\"\n  },\n  \"useDiceRoller\": true,\n  \"yamlStatblocks\": true\n}\n"
  },
  {
    "path": "src/test/resources/5e/sources-images.yaml",
    "content": "---\nsources:\n  reference:\n  - ALL\ninclude:\n  - classtype|artificer|tce\n  - classtype|barbarian|phb\n  - classtype|barbarian|xphb\n  - classtype|bard|phb\n  - classtype|bard|xphb\n  - classtype|cleric|phb\n  - classtype|cleric|xphb\n  - classtype|druid|phb\n  - classtype|druid|xphb\n  - classtype|expert sidekick|tce\n  - classtype|fighter|phb\n  - classtype|fighter|xphb\n  - classtype|monk|phb\n  - classtype|monk|xphb\n  - classtype|mystic|uathemysticclass\n  - classtype|paladin|phb\n  - classtype|paladin|xphb\n  - classtype|ranger|phb\n  - classtype|ranger|xphb\n  - classtype|rogue|phb\n  - classtype|rogue|xphb\n  - classtype|sorcerer|phb\n  - classtype|sorcerer|xphb\n  - classtype|spellcaster sidekick|tce\n  - classtype|warlock|phb\n  - classtype|warlock|xphb\n  - classtype|warrior sidekick|tce\n  - classtype|wizard|phb\n  - classtype|wizard|xphb\ntemplate:\n  background: \"examples/templates/tools5e/images-background2md.txt\"\n  class: \"examples/templates/tools5e/images-class2md.txt\"\n  item: \"examples/templates/tools5e/images-item2md.txt\"\n  monster: \"examples/templates/tools5e/images-monster2md.txt\"\n  object: \"examples/templates/tools5e/images-object2md.txt\"\n  race: \"examples/templates/tools5e/images-race2md.txt\"\n  spell: \"examples/templates/tools5e/images-spell2md.txt\"\n  subclass: \"examples/templates/tools5e/images-subclass2md.txt\"\n  vehicle: \"examples/templates/tools5e/images-vehicle2md.txt\"\nuseDiceRoller: true\n"
  },
  {
    "path": "src/test/resources/5e/sources-single.yaml",
    "content": "sources:\n  book:\n  - ERLW\n"
  },
  {
    "path": "src/test/resources/5e/sources-templates.json",
    "content": "{\n  \"sources\": {\n    \"reference\": [\n      \"srd\",\n      \"basicrules\"\n    ]\n  },\n  \"template\": {\n    \"background\": \"src/test/resources/other/background.txt\",\n    \"class\": \"src/test/resources/other/class.txt\",\n    \"deity\": \"src/test/resources/other/deity.txt\",\n    \"feat\": \"src/test/resources/other/feat.txt\",\n    \"index\": \"src/test/resources/other/index.txt\",\n    \"item\": \"src/test/resources/other/item.txt\",\n    \"monster\": \"src/test/resources/other/monster-all.txt\",\n    \"note\": \"src/test/resources/other/note.txt\",\n    \"race\": \"src/test/resources/other/race.txt\",\n    \"spell\": \"src/test/resources/other/spell.txt\",\n    \"subclass\": \"src/test/resources/other/subclass.txt\"\n  }\n}\n"
  },
  {
    "path": "src/test/resources/5e/sources-ua.json",
    "content": "{\n  \"from\" : [\n    \"PHB\",\n    \"DMG\",\n    \"UADowntime\",\n    \"UAEncounterBuilding\",\n    \"UAIntoTheWild\",\n    \"UAQuickCharacters\",\n    \"UATrapsRevisited\",\n    \"UAWhenArmiesClash\",\n    \"XUA2022CharacterOptions\",\n    \"XUA2022ExpertClasses\",\n    \"XUA2022ClericAndRevisedSpecies\",\n    \"XUA2023BastionsAndCantrips\",\n    \"XUA2023DruidAndPaladin\",\n    \"XUA2023PlayersHandbookP5\",\n    \"XUA2023PlayersHandbookP6\",\n    \"XUA2023PlayersHandbookP7\"\n  ],\n  \"paths\" : {\n    \"compendium\" : \"/\",\n    \"rules\" : \"/rules/\"\n  }\n}\n"
  },
  {
    "path": "src/test/resources/5e/sources.json",
    "content": "{\n  \"sources\": {\n    \"book\": [\n      \"PHB\",\n      \"DMG\",\n      \"XGE\",\n      \"TCE\"\n    ],\n    \"adventure\": [\n      \"WbtW\"\n    ]\n  },\n  \"include\": [\n    \"race|changeling|mpmm\"\n  ],\n  \"exclude\": [\n    \"monster|expert|dc\",\n    \"monster|expert|sdw\",\n    \"monster|expert|slw\"\n  ],\n  \"excludePattern\": [\n    \"race\\\\|.*\\\\|dmg\"\n  ]\n}\n"
  },
  {
    "path": "src/test/resources/5e-sourceTypes.json",
    "content": "{\n  \"adventure\" : [\n    \"LMoP\",\n    \"HotDQ\",\n    \"RoT\",\n    \"PotA\",\n    \"OotA\",\n    \"CoS\",\n    \"SKT\",\n    \"TftYP-TSC\",\n    \"TftYP-TFoF\",\n    \"TftYP-THSoT\",\n    \"TftYP-WPM\",\n    \"TftYP-DiT\",\n    \"TftYP-AtG\",\n    \"TftYP-ToH\",\n    \"ToA\",\n    \"TTP\",\n    \"TLK\",\n    \"XMtS\",\n    \"WDH\",\n    \"LLK\",\n    \"WDMM\",\n    \"KKW\",\n    \"AZfyT\",\n    \"GoS\",\n    \"HftT\",\n    \"OoW\",\n    \"DIP\",\n    \"SLW\",\n    \"SDW\",\n    \"DC\",\n    \"BGDIA\",\n    \"LR\",\n    \"IMR\",\n    \"EFR\",\n    \"RMBRE\",\n    \"ToR\",\n    \"DD\",\n    \"FS\",\n    \"US\",\n    \"MOT-NSS\",\n    \"IDRotF\",\n    \"CM\",\n    \"HoL\",\n    \"RtG\",\n    \"AitFR-ISF\",\n    \"AitFR-THP\",\n    \"AitFR-AVT\",\n    \"AitFR-DN\",\n    \"AitFR-FCD\",\n    \"NRH-TCMC\",\n    \"NRH-AVitW\",\n    \"NRH-ASS\",\n    \"NRH-CoI\",\n    \"NRH-TLT\",\n    \"NRH-AWoL\",\n    \"NRH-AT\",\n    \"WBtW\",\n    \"SCC-CK\",\n    \"SCC-HfMT\",\n    \"SCC-TMM\",\n    \"SCC-ARiR\",\n    \"CRCotN\",\n    \"JttRC\",\n    \"DoSI\",\n    \"SjA\",\n    \"LoX\",\n    \"DSotDQ\",\n    \"KftGV\",\n    \"GotSF\",\n    \"PaBTSO\",\n    \"LK\",\n    \"ToFW\",\n    \"CoA\",\n    \"PiP\",\n    \"HFStCM\",\n    \"DitLCoT\",\n    \"LRDT\",\n    \"VNotEE\",\n    \"VEoR\",\n    \"QftIS\",\n    \"UtHftLH\",\n    \"ScoEE\",\n    \"HBTD\",\n    \"BQGT\",\n    \"DrDe-DaS\",\n    \"DrDe-BD\",\n    \"DrDe-TWoO\",\n    \"DrDe-FWtVC\",\n    \"DrDe-TDoN\",\n    \"DrDe-TFV\",\n    \"DrDe-BtS\",\n    \"DrDe-SD\",\n    \"DrDe-ACfaS\",\n    \"DrDe-DotSC\",\n    \"HotB\",\n    \"WttHC\",\n    \"FRAiF-TLLoL\",\n    \"FFotR\"\n  ],\n  \"book\" : [\n    \"PHB\",\n    \"MM\",\n    \"DMG\",\n    \"Screen\",\n    \"SCAG\",\n    \"PS-Z\",\n    \"PS-I\",\n    \"AL\",\n    \"VGM\",\n    \"PS-K\",\n    \"PS-A\",\n    \"OGA\",\n    \"XGE\",\n    \"PS-X\",\n    \"MTF\",\n    \"PS-D\",\n    \"GGR\",\n    \"SAC\",\n    \"AI\",\n    \"AWM\",\n    \"RMR\",\n    \"ERLW\",\n    \"EGW\",\n    \"MOT\",\n    \"ScreenDungeonKit\",\n    \"HF\",\n    \"ScreenWildernessKit\",\n    \"TCE\",\n    \"MGELFT\",\n    \"VRGR\",\n    \"DoD\",\n    \"MaBJoV\",\n    \"FTD\",\n    \"SCC\",\n    \"MPMM\",\n    \"TD\",\n    \"ScreenSpelljammer\",\n    \"AAG\",\n    \"BAM\",\n    \"HAT-TG\",\n    \"BGG\",\n    \"MCV4EC\",\n    \"MPP\",\n    \"SatO\",\n    \"AATM\",\n    \"HFFotM\",\n    \"BMT\",\n    \"DMTCRG\",\n    \"PaF\",\n    \"XPHB\",\n    \"XDMG\",\n    \"XScreen\",\n    \"XMM\",\n    \"XSAC\",\n    \"ABH\",\n    \"FRAiF\",\n    \"FRHoF\",\n    \"NF\",\n    \"LFL\",\n    \"EFA\",\n    \"CaBoMP\"\n  ]\n}"
  },
  {
    "path": "src/test/resources/dice-roller-fs.json",
    "content": "{\n  \"useDiceRoller\": true,\n  \"yamlStatblocks\": true\n}\n"
  },
  {
    "path": "src/test/resources/dice-roller.json",
    "content": "{\n  \"useDiceRoller\": true,\n  \"yamlStatblocks\": false\n}\n"
  },
  {
    "path": "src/test/resources/other/background.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses: json5e-background\ntags: \n- test\n{#if resource.tags }{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# [{resource.name}]({resource.vaultPath})\n*Source: {resource.source}*  \n\n{resource.text}\n"
  },
  {
    "path": "src/test/resources/other/class.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses: json5e-class\ntags:\n- test\n{#if resource.tags }{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# [{resource.name}]({resource.vaultPath})\n*Source: {resource.source}*  \n\n{#if resource.classProgression }\n{resource.classProgression}\n\n{/if}\n## Hit Points\n\n{#if resource.hitDice }\n- **Hit Dice**: 1d{resource.hitDice} per {resource.name} level\n- **Hit Points at First Level:** {resource.hitDice} + CON\n- **Hit Points at Higher Levels:** add {resource.hitRollAverage} OR 1d{resource.hitDice} + CON  (minimum of 1)\n{#else}\n- **Hit Dice**: *x* = hit dice specified in the sidekick's statblock (human, gnome, kobold, etc.)\n- **Hit Points at First Level:** *x* + CON\n- **Hit Points at Higher Levels:** add 1d*x* + CON (minimum of 1)\n{/if}\n\n## Starting a {resource.name}\n\n{resource.startingEquipment}\n\n{#if resource.multiclassing }\n## Multiclassing {resource.name}\n\n{resource.multiclassing}\n{/if}\n\n{resource.text}\n"
  },
  {
    "path": "src/test/resources/other/deity.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses: json5e-deity\ntags: \n- test\n{#if resource.tags }{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# [{resource.name}]({resource.vaultPath})\n*Source: {resource.source}* {#if resource.image }\n{resource.image} {/if}\n\n{#if resource.alignment }\n- **Alignment**: {resource.alignment}\n{/if}{#if resource.category }\n- **Category**: {resource.category}\n{/if}{#if resource.domains }\n- **Domains**: {resource.domains}\n{/if}{#if resource.pantheon }\n- **Pantheon**: {resource.pantheon}\n{/if}{#if resource.province }\n- **Province**: {resource.province}\n{/if}{#if resource.symbol }\n- **Symbol**: {resource.symbol}\n{/if}\n\n{resource.text}\n"
  },
  {
    "path": "src/test/resources/other/feat.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses: json5e-feat\ntags: \n- test\n{#if resource.tags }{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# [{resource.name}]({resource.vaultPath})\n*Source: {resource.source}*  \n\n{#if resource.level || resource.prerequisite}\n{#if resource.prerequisite}\n***Prerequisites*** {resource.prerequisite}\n{/if}\n{#if resource.level}\n***Level*** {resource.level}\n{/if}\n\n{/if}\n{resource.text}\n\n"
  },
  {
    "path": "src/test/resources/other/index.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses: json5e-note\ntags: \n- test\n---\n# Index of {name}\n\n{#for mapping in resources}\n- [{mapping.title}]({mapping.fileName})\n{/for}\n"
  },
  {
    "path": "src/test/resources/other/item.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses: json5e-item\n{#if resource.costCp }\ncost: {resource.costCp}\n{/if}\ntags: \n- test\n{#if resource.tags }{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# [{resource.name}]({resource.vaultPath})\n{#if resource.detail }*{resource.detail}*  {/if}\n\n{#if resource.armorClass }\n- **Armor Class**: {resource.armorClass}\n{#else if resource.damage }{#if resource.damage2h }\n- **Damage**:\n  - One-handed: {resource.damage}\n  - Two-handed: {resource.damage2h}\n{#else}\n- **Damage**: {resource.damage}\n{/if}{#if resource.range }\n- **Range**: {resource.range}\n{/if}{/if}{#if resource.properties }\n- **Properties**: {resource.properties}\n{/if}{#if resource.strengthRequirement }\n- **Strength**: Requires {resource.strengthRequirement} STR.\n{/if}{#if resource.stealthPenalty }\n- **Stealth**: The wearer has disadvantage on Stealth (DEX) checks.\n{/if}\n- **Cost**: {#if resource.cost }{resource.cost}{#else}⏤{/if}\n- **Weight**: {#if resource.weight }{resource.weight} lbs.{#else}⏤{/if}\n{#if resource.text }\n\n{resource.text}\n{/if}\n\n*Source: {resource.source}*\n"
  },
  {
    "path": "src/test/resources/other/monster-all.txt",
    "content": "---\nobsidianUIMode: preview\ncssclass: json5e-monster\nstatblock: inline\n{#if resource.tags }\ntags:\n- test\n{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n\n# [{resource.name}]({resource.vaultPath})\n*Source: {resource.source}*  \n\n{#if resource.description }\n{resource.description}\n\n{/if}\n```statblock\n{resource.5eStatblockYaml}\n```\n^statblock\n\n```ad-statblock\ntitle: {resource.name}{#if resource.token}\n![{resource.token.title}]({resource.token.vaultPath}#token){/if}\n*{resource.size} {resource.fullType}, {resource.alignment}*\n\n- **Armor Class** {#if resource.ac }{resource.ac} {/if}{#if resource.acText }({resource.acText}){/if}\n- **Hit Points** {resource.hp or ' '} {#if resource.hitDice }(`{resource.hitDice}`){/if} {#if resource.hpText }({resource.hpText}){/if}\n- **Speed** {resource.speed}\n\n|STR|DEX|CON|INT|WIS|CHA|\n|:---:|:---:|:---:|:---:|:---:|:---:|\n|{resource.scores}|\n\n- **Proficiency Bonus** {resource.pb}\n- **Saving Throws** {#if resource.savingThrows }{resource.savingThrows}{#else}⏤{/if}\n- **Skills** {#if resource.skills }{resource.skills}{#else}⏤{/if}\n- **Senses** {#if resource.senses }{resource.senses}, {/if}passive Perception {resource.passive}\n{#if resource.vulnerable }\n- **Damage Vulnerabilities** {resource.vulnerable}\n{/if}{#if resource.resist}\n- **Damage Resistances** {resource.resist}\n{/if}{#if resource.immune}\n- **Damage Immunities** {resource.immune}\n{/if}{#if resource.conditionImmune}\n- **Condition Immunities** {resource.conditionImmune}\n{/if}\n- **Languages** {#if resource.languages }{resource.languages}{#else}—{/if}\n- **Challenge** {resource.cr}\n{#if resource.trait}\n\n## Traits\n{#for trait in resource.trait}\n\n{#if trait.name }***{trait.name}.*** {/if}{trait.desc}\n{/for}{/if}{#if resource.action}\n\n## Actions\n{#for action in resource.action}\n\n{#if action.name }***{action.name}.*** {/if}{action.desc}\n{/for}{/if}{#if resource.bonusAction}\n\n## Bonus Actions\n{#for bonusAction in resource.bonusAction}\n\n{#if bonusAction.name }***{bonusAction.name}.*** {/if}{bonusAction.desc}\n{/for}{/if}{#if resource.reaction}\n\n## Reactions\n{#for reaction in resource.reaction}\n\n{#if reaction.name }***{reaction.name}.*** {/if}{reaction.desc}\n{/for}{/if}{#if resource.legendary}\n\n## Legendary Actions\n{#for legendary in resource.legendary}\n\n{#if legendary.name }***{legendary.name}.*** {/if}{legendary.desc}\n{/for}{/if}{#if resource.legendaryGroup}{#for group in resource.legendaryGroup}\n\n## {group.name}\n\n{group.desc}\n{/for}{/if}\n```\n^statblock-manual\n\n{#if resource.environment }\n\n## Environment\n\n{resource.environment}\n{/if}\n"
  },
  {
    "path": "src/test/resources/other/note.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses: json5e-note\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\ntags: \n- test\n---\n# [{resource.name}]({resource.vaultPath})\n{#if resource.source }*Source: {resource.source}* {/if}\n\n{resource.text}\n"
  },
  {
    "path": "src/test/resources/other/race.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses: json5e-race\ntags: \n- test\n{#if resource.tags }{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# [{resource.name}]({resource.vaultPath})\n*Source: {resource.source}*  \n\n- **Ability Scores**: {resource.ability}\n{#if resource.type}\n- **Creature Type**: {resource.type}\n{/if}\n- **Size**: {resource.size}\n- **Speed**: {resource.speed}\n{#if resource.spellcasting}\n- **Spellcasting**: {resource.spellcasting}\n{/if}\n\n## Traits\n\n{resource.traits}\n{#if resource.description}\n\n## Description\n\n{resource.description}\n{/if}\n"
  },
  {
    "path": "src/test/resources/other/spell.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses: json5e-spell\ntags: \n- test\n{#if resource.tags }{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# [{resource.name}]({resource.vaultPath})\n*{resource.level}, {resource.school}{#if resource.ritual} (ritual){/if}*  \n\n- **Casting time:** {resource.time}{#if resource.ritual} unless cast as a ritual{/if}\n- **Range:** {resource.range}\n- **Components:** {resource.components}\n- **Duration:** {resource.duration}\n\n{resource.text}\n\n**Classes**: {resource.classes}\n\nSource: {resource.source}\n"
  },
  {
    "path": "src/test/resources/other/subclass.txt",
    "content": "---\nobsidianUIMode: preview\ncssclasses: json5e-class\ntags: \n- test\n{#if resource.tags }{#for tag in resource.tags}\n- {tag}\n{/for}{/if}\naliases:\n{#each resource.aliases}\n- {it}\n{/each}\n---\n# [{resource.name}]({resource.vaultPath})\n*{resource.parentClass}: {resource.subclassTitle}*  \n*Source: {resource.source}*  \n\n{#if resource.classProgression }\n{resource.classProgression}\n\n{/if}\n\n{resource.text}\n"
  },
  {
    "path": "src/test/resources/paths.json",
    "content": "{\n  \"paths\": {\n    \"rules\": \"rules/\",\n    \"compendium\": \"\"\n  }\n}\n"
  },
  {
    "path": "src/test/resources/pf2e.json",
    "content": "{\n  \"from\": [\n    \"AAWS\",\n    \"AFoF\",\n    \"APG\",\n    \"AV0\",\n    \"AV1\",\n    \"AV2\",\n    \"AV3\",\n    \"AVH\",\n    \"AoA0\",\n    \"AoA1\",\n    \"AoA2\",\n    \"AoA3\",\n    \"AoA4\",\n    \"AoA5\",\n    \"AoA6\",\n    \"AoE0\",\n    \"AoE1\",\n    \"AoE2\",\n    \"AoE3\",\n    \"AoE4\",\n    \"AoE5\",\n    \"AoE6\",\n    \"B1\",\n    \"B2\",\n    \"B3\",\n    \"BB\",\n    \"BL0\",\n    \"BL1\",\n    \"BL2\",\n    \"BL3\",\n    \"BL4\",\n    \"BL5\",\n    \"BL6\",\n    \"BotD\",\n    \"CFD\",\n    \"CHD\",\n    \"CRB\",\n    \"DA\",\n    \"EC0\",\n    \"EC1\",\n    \"EC2\",\n    \"EC3\",\n    \"EC4\",\n    \"EC5\",\n    \"EC6\",\n    \"FRP0\",\n    \"FRP1\",\n    \"FRP2\",\n    \"FRP3\",\n    \"FoP\",\n    \"GMG\",\n    \"GW0\",\n    \"GW1\",\n    \"GW2\",\n    \"GnG\",\n    \"HPD\",\n    \"LOACLO\",\n    \"LOAG\",\n    \"LOCG\",\n    \"LOGM\",\n    \"LOGMWS\",\n    \"LOIL\",\n    \"LOKL\",\n    \"LOL\",\n    \"LOME\",\n    \"LOMM\",\n    \"LOPSG\",\n    \"LOTG\",\n    \"LOTGB\",\n    \"LOWG\",\n    \"LTiBA\"\n  ]\n}\n"
  },
  {
    "path": "src/test/resources/sourcemap.txt",
    "content": "# Source mapping\n\n- [5etools](#source-name-mapping-for-5etools)\n- [Pf2eTools](#source-name-mapping-for-pf2etools)\n\nHere is the name/abbreviation mapping for source materials.\n\n_Support content creators. Only use or include sources that you own._\n\n## Source name mapping for 5etools\n\n- **2014** (sources/reference): \"srd\", \"basicrules\"\n- **2024** (sources/reference): \"srd52\", \"basicRules2024\"\n\n<!--%% 5etools %% -->\n\n## Source name mapping for Pf2eTools\n\n<!--%% Pf2eTools %% -->\n"
  },
  {
    "path": "src/test/resources/sources-bad-template.json",
    "content": "{\n  \"from\": [\n    \"ALL\"\n  ],\n  \"template\": {\n    \"background\": \"garbage\",\n    \"glory\": \"examples/templates/tools5e/images-background2md.txt\" \n  }\n}\n"
  },
  {
    "path": "src/test/resources/sources-from-all.json",
    "content": "{\n  \"from\": [\n    \"ALL\"\n  ]\n}\n"
  }
]